├── .eslintrc.js ├── .github └── workflows │ ├── ci.yaml │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── coverage.svg ├── deno ├── build.mjs └── lib │ ├── __tests__ │ ├── client.test.ts │ ├── match.test.ts │ ├── param.test.ts │ ├── parse.test.ts │ ├── pets_endpoints.test.ts │ ├── pets_raw.test.ts │ ├── project.test.ts │ ├── router_body.test.ts │ ├── router_minimal.test.ts │ └── type.test.ts │ ├── api.ts │ ├── client.ts │ ├── component.ts │ ├── data │ └── petstore.ts │ ├── deps.ts │ ├── dsl.ts │ ├── index.ts │ ├── integer.ts │ ├── match.ts │ ├── mod.ts │ ├── model.ts │ ├── openapi.ts │ ├── parameter.ts │ ├── playground.ts │ ├── reference.ts │ ├── utils.ts │ └── utils │ └── openapi3-ts │ ├── OpenApi.ts │ └── SpecificationExtension.ts ├── example ├── index.html ├── mod.ts ├── model.ts └── router.ts ├── images └── pets_swagger.png ├── jest.config.json ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ ├── client.test.ts │ ├── match.test.ts │ ├── param.test.ts │ ├── parse.test.ts │ ├── pets_endpoints.test.ts │ ├── pets_raw.test.ts │ ├── project.test.ts │ ├── router_body.test.ts │ ├── router_minimal.test.ts │ └── type.test.ts ├── api.ts ├── client.ts ├── component.ts ├── data │ └── petstore.ts ├── deps.ts ├── dsl.ts ├── index.ts ├── integer.ts ├── match.ts ├── model.ts ├── openapi.ts ├── parameter.ts ├── playground.ts ├── reference.ts ├── utils.ts └── utils │ └── openapi3-ts │ ├── OpenApi.ts │ └── SpecificationExtension.ts ├── tsconfig.base.json ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── tsconfig.types.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, node: true }, 3 | root: true, 4 | parser: "@typescript-eslint/parser", 5 | plugins: [ 6 | "@typescript-eslint", 7 | "import", 8 | "simple-import-sort", 9 | "unused-imports", 10 | ], 11 | extends: [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 15 | ], 16 | rules: { 17 | "import/order": 0, // turn off in favor of eslint-plugin-simple-import-sort 18 | "import/no-unresolved": 0, 19 | "import/no-duplicates": 1, 20 | 21 | /** 22 | * eslint-plugin-simple-import-sort @see https://github.com/lydell/eslint-plugin-simple-import-sort 23 | */ 24 | "sort-imports": 0, // we use eslint-plugin-import instead 25 | "simple-import-sort/imports": 1, 26 | "simple-import-sort/exports": 1, 27 | 28 | /** 29 | * @typescript-eslint/eslint-plugin @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin 30 | */ 31 | "@typescript-eslint/no-namespace": "off", 32 | "@typescript-eslint/explicit-module-boundary-types": "off", 33 | "@typescript-eslint/no-explicit-any": "off", 34 | "@typescript-eslint/ban-types": "off", 35 | "@typescript-eslint/no-unused-vars": "off", 36 | "@typescript-eslint/no-empty-function": "off", 37 | "@typescript-eslint/ban-ts-comment": "off", 38 | "@typescript-eslint/no-non-null-assertion": "off", 39 | "@typescript-eslint/no-empty-interface": "off", 40 | /** 41 | * ESLint core rules @see https://eslint.org/docs/rules/ 42 | */ 43 | "no-case-declarations": "off", 44 | "no-empty": "off", 45 | "no-useless-escape": "off", 46 | "no-control-regex": "off", 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: test and build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: 14.x 14 | - uses: denolib/setup-deno@v2 15 | with: 16 | deno-version: v1.4.6 17 | - run: yarn install 18 | - run: yarn build 19 | - run: yarn test -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: 4 | push: 5 | create: 6 | pull_request: 7 | schedule: 8 | - cron: '44 4 * * SAT' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test-node: 13 | runs-on: Ubuntu-20.04 14 | strategy: 15 | matrix: 16 | node: [ '14' ] 17 | typescript: [ '4.1', '4.2' ] 18 | name: Test with TypeScript ${{ matrix.typescript }} on Node ${{ matrix.node }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node }} 24 | - run: yarn install 25 | - run: yarn add typescript@${{ matrix.typescript }} 26 | - run: yarn build 27 | - run: yarn test 28 | 29 | test-deno: 30 | runs-on: Ubuntu-20.04 31 | strategy: 32 | matrix: 33 | deno: [ "v1.8.1", "v1.x" ] 34 | name: Test with Deno ${{ matrix.deno }} 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: denolib/setup-deno@v2 38 | with: 39 | deno-version: ${{ matrix.deno }} 40 | - run: deno --version 41 | - run: deno test 42 | working-directory: ./deno/lib 43 | - run: deno run ./index.ts 44 | working-directory: ./deno/lib 45 | - run: deno run ./mod.ts 46 | working-directory: ./deno/lib 47 | - run: | 48 | deno bundle ./mod.ts ./bundle.js 49 | deno run ./bundle.js 50 | working-directory: ./deno/lib 51 | 52 | lint: 53 | runs-on: Ubuntu-20.04 54 | strategy: 55 | matrix: 56 | node: [ '14' ] 57 | name: Lint on Node ${{ matrix.node }} 58 | steps: 59 | - uses: actions/checkout@v2 60 | - uses: actions/setup-node@v1 61 | with: 62 | node-version: ${{ matrix.node }} 63 | - run: yarn install 64 | - run: yarn check:format 65 | - run: yarn check:lint 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/.DS_Store 3 | node_modules 4 | /lib 5 | /coverage 6 | **/coverage 7 | .vscode 8 | .idea 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Flock. Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NPM version 2 | coverage 3 | 4 | 5 | # Zod-endpoints 6 | Contract first strictly typed endpoints. By defining endpoints as a zod schema, all the requests and responses can be checked and parsed at runtime. Moreover, the TypeScript compiler can check the in- and output types of the endpoints. 7 | 8 | In this way the problem space of your application will become much smaller to begin with. By using zod-endpoints you can rely on the types during development and on validation at runtime. This yields requests and responses you can trust. The focus can shift more to defining business logic instead of input validation and error handling. 9 | 10 | The schema can be used as a contract between consumer and producer. Drivers can be generated from the contract which ensures proper communication between a client and server. 11 | 12 | ## Example project 13 | 14 | [Example](https://github.com/flock-community/zod-endpoints-example) 15 | 16 | ## Simplified model 17 | 18 | Zod-endpoints is based on a type representation of a http schema. Below a simplyfied version of the model. The full model can be found [here](src/model.ts). The model is a union of requests which contains a union of response objects. Both request and response contain a union of body types. 19 | 20 | ````ts 21 | type Body = { 22 | type: "applictions/json" | "plain/html" 23 | content: any 24 | } 25 | 26 | type Request = { 27 | method: "GET" | "POST" | "PUT" | "DELETE" 28 | path: [...string[]] 29 | body: Body | ...Body 30 | } 31 | 32 | type Response = { 33 | status: number 34 | body: Body | ...Body 35 | } 36 | 37 | type Http = Request & { 38 | responses: Response | ...Response 39 | } 40 | 41 | type Schema = Http | ...Http 42 | ```` 43 | 44 | ## Getting started 45 | First step is to define an endpoint by making use of the [zod-endpoints dsl](src/dsl.ts). Below you can find an example of a simple example. This example contains two endpoints to get and create a project. 46 | 47 | ### Define endpoints 48 | ````ts 49 | import * as z from "zod-endpoints"; 50 | 51 | const project = z.object({ 52 | id: z.string().uuid(), 53 | name: z.string(), 54 | }) 55 | 56 | const schema = z.endpoints([ 57 | z.endpoint({ 58 | name: "GET_PROJECT", 59 | method: "GET", 60 | path: [z.literal("projects"), z.string().uuid()], 61 | responses: [ 62 | z.response({ 63 | status: 200, 64 | body:{ 65 | type: "application/json", 66 | content: project 67 | } 68 | }), 69 | ], 70 | }), 71 | z.endpoint({ 72 | name: "CREATE_PROJECT", 73 | method: "POST", 74 | path: [z.literal("projects")], 75 | body:{ 76 | type: "application/json", 77 | content: project 78 | }, 79 | responses: [ 80 | z.response({ 81 | status: 201, 82 | }), 83 | ], 84 | }), 85 | ]); 86 | ```` 87 | 88 | ## Api 89 | The endpoints can be convert into a service or a client with the [Api](src/api.ts) type. 90 | 91 | ### Server 92 | For the type transforms the schema into an object of the requests. The key of the object is the name of the route the value is a function from the request to a union of the responses. This object is strict typed and exhaustive. 93 | 94 | ```ts 95 | const service = { 96 | findProjectById: (id:string):Project => {}, 97 | createProject: (project:Project) => {}, 98 | } 99 | ```` 100 | 101 | ```ts 102 | import * as z from "zod-endpoints"; 103 | 104 | const server: z.Api = { 105 | "GET_PROJECT": ({path}) => findProjectById(path[1]).then(project => ({ 106 | status: 200, 107 | body:{ 108 | type: "application/json", 109 | content:project 110 | } 111 | })), 112 | "CREATE_PROJECT": ({body}) => createProject(body).Promise.resolve({ 113 | status: 201 114 | }), 115 | }; 116 | ``` 117 | 118 | ### Client 119 | The client implementation 120 | 121 | ```ts 122 | const http = (req: z.ApiRequest) => { 123 | fetch(req.path.join('/'), { 124 | method: req.method 125 | }) 126 | } 127 | ```` 128 | 129 | ```ts 130 | import * as z from "zod-endpoints"; 131 | 132 | const client: z.Api = { 133 | "GET_PROJECT": (req) => http(req), 134 | "CREATE_PROJECT": (req) => http(req) 135 | }; 136 | ``` 137 | 138 | ## Documentation 139 | Zod endpoints is fully compatible with [open api specification](https://www.openapis.org/). The schema can be transformed into open api json. For example with Swagger this can be presented as a documentation website. 140 | 141 | ```ts 142 | const docs = z.openApi(schema) 143 | ```` 144 | ![GitHub Logo](images/pets_swagger.png) 145 | 146 | -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | Coverage: 87.77%Coverage87.77% -------------------------------------------------------------------------------- /deno/build.mjs: -------------------------------------------------------------------------------- 1 | // This script expects to be run via `yarn build:deno`. 2 | // 3 | // Although this script generates code for use in Deno, this script itself is 4 | // written for Node so that contributors do not need to install Deno to build. 5 | // 6 | // @ts-check 7 | 8 | import { 9 | mkdirSync, 10 | readdirSync, 11 | readFileSync, 12 | statSync, 13 | writeFileSync, 14 | } from "fs"; 15 | import { dirname } from "path"; 16 | 17 | // Node's path.join() normalize explicitly-relative paths like "./index.ts" to 18 | // paths like "index.ts" which don't work as relative ES imports, so we do this. 19 | const join = (/** @type string[] */ ...parts) => parts.join("/"); 20 | 21 | const projectRoot = process.cwd(); 22 | const nodeSrcRoot = join(projectRoot, "src"); 23 | const denoLibRoot = join(projectRoot, "deno", "lib"); 24 | 25 | const walkAndBuild = (/** @type string */ dir) => { 26 | for (const entry of readdirSync(join(nodeSrcRoot, dir), { 27 | withFileTypes: true, 28 | encoding: "utf-8", 29 | })) { 30 | if (entry.isDirectory()) { 31 | walkAndBuild(join(dir, entry.name)); 32 | } else if (entry.isFile() && entry.name.endsWith(".ts")) { 33 | const nodePath = join(nodeSrcRoot, dir, entry.name); 34 | const denoPath = join(denoLibRoot, dir, entry.name); 35 | 36 | const nodeSource = readFileSync(nodePath, { encoding: "utf-8" }); 37 | 38 | const denoSource = nodeSource.replace( 39 | /^(?:import|export)[\s\S]*?from\s*['"]([^'"]*)['"];$/gm, 40 | (line, target) => { 41 | if (target === "@jest/globals") { 42 | return `import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";\nconst test = Deno.test;`; 43 | } 44 | 45 | if (target === "zod") { 46 | return `export * from "https://raw.githubusercontent.com/colinhacks/zod/master/deno/lib/mod.ts";`; 47 | } 48 | 49 | const targetNodePath = join(dirname(nodePath), target); 50 | const targetNodePathIfFile = targetNodePath + ".ts"; 51 | const targetNodePathIfDir = join(targetNodePath, "index.ts"); 52 | try { 53 | 54 | if (statSync(targetNodePathIfFile)?.isFile()) { 55 | return line.replace(target, target + ".ts"); 56 | } 57 | } catch (error) { 58 | if (error?.code !== "ENOENT") { 59 | throw error; 60 | } 61 | } 62 | 63 | try { 64 | if (statSync(targetNodePathIfDir)?.isFile()) { 65 | return line.replace(target, join(target, "index.ts")); 66 | } 67 | } catch (error) { 68 | if (error?.code !== "ENOENT") { 69 | throw error; 70 | } 71 | } 72 | 73 | console.warn(`Skipping non-resolvable import:\n ${line}`); 74 | return line; 75 | } 76 | ); 77 | 78 | mkdirSync(dirname(denoPath), { recursive: true }); 79 | writeFileSync(denoPath, denoSource, { encoding: "utf-8" }); 80 | } 81 | } 82 | }; 83 | 84 | walkAndBuild(""); 85 | 86 | writeFileSync(join(denoLibRoot, "mod.ts"), `export * from "./index.ts";\n`, { 87 | encoding: "utf-8", 88 | }); -------------------------------------------------------------------------------- /deno/lib/__tests__/client.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | 7 | const project = z.object({ 8 | id: z.string().uuid(), 9 | name: z.string(), 10 | }); 11 | 12 | const error = z.object({ 13 | code: z.number(), 14 | message: z.string(), 15 | }); 16 | 17 | const errorResponse = z.response({ 18 | status: 500, 19 | description: "Error occurred", 20 | body: z.body({ 21 | type: "application/json", 22 | content: error, 23 | }), 24 | }); 25 | 26 | const schema = z.endpoints([ 27 | z.endpoint({ 28 | name: "GET_PROJECT", 29 | method: "GET", 30 | path: z.path("projects", z.string().uuid()), 31 | responses: [ 32 | z.response({ 33 | description: "Found project", 34 | status: 200, 35 | body: z.body({ 36 | type: "application/json", 37 | content: project, 38 | }), 39 | }), 40 | z.response({ 41 | description: "Not found", 42 | status: 404, 43 | body: z.body({ 44 | type: "application/json", 45 | content: error, 46 | }), 47 | }), 48 | errorResponse, 49 | ], 50 | }), 51 | z.endpoint({ 52 | name: "LIST_PROJECT", 53 | method: "GET", 54 | path: z.path("projects"), 55 | headers: {}, 56 | responses: [ 57 | z.response({ 58 | description: "Found project", 59 | status: 200, 60 | body: z.body({ 61 | type: "application/json", 62 | content: z.array(project), 63 | }), 64 | }), 65 | errorResponse, 66 | ], 67 | }), 68 | z.endpoint({ 69 | name: "CREATE_PROJECT", 70 | method: "POST", 71 | path: z.path("projects"), 72 | body: z.body({ 73 | type: "application/json", 74 | content: project, 75 | }), 76 | responses: [ 77 | z.response({ 78 | description: "Created project", 79 | status: 201, 80 | }), 81 | errorResponse, 82 | ], 83 | }), 84 | ]); 85 | 86 | const id = "1a2c8758-e223-11eb-ba80-0242ac130004" as string; 87 | const state: z.infer[] = []; 88 | 89 | // @ts-ignore 90 | const client: z.Client = (req) => { 91 | console.log(req); 92 | if (req && req.body && req.method === "POST" && req.path[0] == "projects") { 93 | state.push(req.body.content); 94 | return Promise.resolve({ 95 | status: 201, 96 | body: { 97 | type: "application/json", 98 | content: state, 99 | }, 100 | headers: {}, 101 | }); 102 | } 103 | 104 | if (req.method === "GET" && req.path[1] == id) { 105 | return Promise.resolve({ 106 | status: 200, 107 | body: { 108 | type: "application/json", 109 | content: state.find((it) => it.id === id), 110 | }, 111 | headers: {}, 112 | }); 113 | } 114 | 115 | if (req.method === "GET" && req.path[0] == "projects") { 116 | return Promise.resolve({ 117 | status: 200, 118 | body: { 119 | type: "application/json", 120 | content: state, 121 | }, 122 | headers: {}, 123 | }); 124 | } 125 | 126 | throw new Error("Cannot respond"); 127 | }; 128 | 129 | test("client", async () => { 130 | const resPost = await client({ 131 | method: "POST", 132 | path: ["projects"], 133 | body: { 134 | type: "application/json", 135 | content: { 136 | id: id, 137 | name: "Todo 12", 138 | }, 139 | }, 140 | query: {}, 141 | headers: {}, 142 | }); 143 | 144 | expect(resPost.status).toBe(201); 145 | 146 | const resGetList = await client({ 147 | method: "GET", 148 | path: ["projects"], 149 | query: {}, 150 | headers: {}, 151 | }); 152 | 153 | expect(resGetList.status).toBe(200); 154 | if (resGetList.status == 200) { 155 | expect(resGetList.body.content.find((it) => it.id === id)?.id).toBe(id); 156 | } 157 | 158 | const resGetId = await client({ 159 | method: "GET", 160 | path: ["projects", id], 161 | query: {}, 162 | headers: {}, 163 | }); 164 | 165 | expect(resGetId.status).toBe(200); 166 | if (resGetId.status == 200) { 167 | expect(resGetId.body.content.id).toBe(id); 168 | } 169 | }); 170 | 171 | test("openapi", () => { 172 | const openApi = z.openApi(schema); 173 | 174 | const assert = Object.entries(openApi.paths).flatMap(([key, value]) => 175 | Object.entries(value).map(([_, req]) => [ 176 | key, 177 | // @ts-ignore 178 | req.parameters?.map((par) => par.name), 179 | ]) 180 | ); 181 | expect(JSON.stringify(assert)).toBe( 182 | JSON.stringify([ 183 | ["/projects/{param_1}", ["param_1"]], 184 | ["/projects", undefined], 185 | ["/projects", undefined], 186 | ]) 187 | ); 188 | }); 189 | -------------------------------------------------------------------------------- /deno/lib/__tests__/match.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | 7 | test("match requset", () => { 8 | const a = z.endpoint({ 9 | name: "A", 10 | method: "GET", 11 | path: [z.literal("a")], 12 | query: { 13 | next: z.parameter(z.string()), 14 | }, 15 | responses: [ 16 | z.response({ 17 | status: 200, 18 | }), 19 | ], 20 | }); 21 | 22 | const b = z.endpoint({ 23 | name: "B", 24 | method: "POST", 25 | path: [z.literal("b")], 26 | query: { 27 | next: z.parameter(z.string().optional()), 28 | }, 29 | responses: [ 30 | z.response({ 31 | status: 200, 32 | body: [ 33 | z.body({ 34 | type: "application/json", 35 | content: z.object({ 36 | b: z.string(), 37 | }), 38 | }), 39 | z.body({ 40 | type: "plain/text", 41 | content: z.object({ 42 | c: z.string(), 43 | }), 44 | }), 45 | ], 46 | }), 47 | ], 48 | }); 49 | const schema = z.union([a, b]); 50 | 51 | const reqA: z.MatchRequest = { 52 | method: "GET", 53 | path: ["a"], 54 | query: { 55 | next: "a", 56 | }, 57 | headers: {}, 58 | }; 59 | 60 | const reqB: z.MatchRequest = { 61 | method: "POST", 62 | path: ["b"], 63 | query: { 64 | next: undefined, 65 | }, 66 | headers: {}, 67 | }; 68 | 69 | expect(z.matchRequest(schema, reqA)).toEqual(a); 70 | expect(z.matchRequest(schema, reqB)).toEqual(b); 71 | }); 72 | -------------------------------------------------------------------------------- /deno/lib/__tests__/param.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | 7 | test("parameter with number", () => { 8 | const n = z 9 | .parameter(z.number().max(100)) 10 | .name("limit") 11 | .description("How many items to return at one time (max 100)"); 12 | 13 | expect(n.parse(50)).toEqual(50); 14 | 15 | try { 16 | n.parse(400); 17 | } catch (err) { 18 | const zerr: z.ZodError = err; 19 | expect(zerr.issues[0].code).toEqual(z.ZodIssueCode.too_big); 20 | expect(zerr.issues[0].message).toEqual( 21 | `Value should be less than or equal to 100` 22 | ); 23 | } 24 | }); 25 | 26 | test("parameter with string", () => { 27 | const s = z 28 | .parameter(z.string().max(7)) 29 | .name("limit") 30 | .description("How many items to return at one time (max 100)"); 31 | 32 | expect(s.parse("123456")).toEqual("123456"); 33 | 34 | try { 35 | s.parse("12345678"); 36 | } catch (err) { 37 | const zerr: z.ZodError = err; 38 | expect(zerr.issues[0].code).toEqual(z.ZodIssueCode.too_big); 39 | expect(zerr.issues[0].message).toEqual( 40 | `Should be at most 7 characters long` 41 | ); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /deno/lib/__tests__/parse.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | 7 | test("parse bigint", () => { 8 | const bigint = z.bigint(); 9 | const res = bigint.parse(BigInt(2)); 10 | expect(res).toEqual(BigInt(2)); 11 | }); 12 | 13 | test("parse pet", () => { 14 | const Pet = z.object({ 15 | id: z.bigint(), 16 | name: z.string(), 17 | tag: z.string().optional(), 18 | }); 19 | const Pets = z.array(z.reference("Pet", Pet)); 20 | 21 | const arr = [ 22 | { id: BigInt(0), name: "a", tag: "Test" }, 23 | { id: BigInt(1), name: "b", tag: "Test" }, 24 | ]; 25 | 26 | expect(Pets.parse(arr)).toEqual([ 27 | { id: BigInt(0), name: "a", tag: "Test" }, 28 | { id: BigInt(1), name: "b", tag: "Test" }, 29 | ]); 30 | }); 31 | -------------------------------------------------------------------------------- /deno/lib/__tests__/pets_endpoints.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import petApi from "../data/petstore.ts"; 6 | import * as z from "../index.ts"; 7 | import { openApi } from "../openapi.ts"; 8 | 9 | const Error = z.object({ 10 | code: z.integer(), 11 | message: z.string(), 12 | }); 13 | 14 | const Pet = z.object({ 15 | id: z.integer("int64"), 16 | name: z.string(), 17 | tag: z.string().optional(), 18 | }); 19 | 20 | const Pets = z.array(z.reference("Pet", Pet)); 21 | 22 | const schema = z.endpoints([ 23 | z.endpoint({ 24 | name: "listPets", 25 | summary: "List all pets", 26 | tags: [z.literal("pets")], 27 | method: "GET", 28 | path: [z.literal("pets")], 29 | query: { 30 | limit: z 31 | .parameter(z.integer("int32").max(100).optional()) 32 | .description("How many items to return at one time (max 100)"), 33 | }, 34 | responses: [ 35 | z.response({ 36 | status: 200, 37 | description: "A paged array of pets", 38 | headers: { 39 | "x-next": z 40 | .parameter(z.string()) 41 | .name("x-next") 42 | .description("A link to the next page of responses"), 43 | }, 44 | body: z.body({ 45 | type: "application/json", 46 | content: z.reference("Pets", Pets), 47 | }), 48 | }), 49 | z.response({ 50 | status: "default", 51 | description: "unexpected error", 52 | body: z.body({ 53 | type: "application/json", 54 | content: z.reference("Error", Error), 55 | }), 56 | }), 57 | ], 58 | }), 59 | 60 | z.endpoint({ 61 | name: "showPetById" as const, 62 | summary: "Info for a specific pet", 63 | tags: [z.literal("pets")], 64 | method: "GET", 65 | path: [ 66 | z.literal("pets"), 67 | z 68 | .parameter(z.string().uuid()) 69 | .name("petId") 70 | .description("The id of the pet to retrieve"), 71 | ], 72 | responses: [ 73 | z.response({ 74 | status: 200, 75 | description: "Expected response to a valid request", 76 | body: z.body({ 77 | type: "application/json", 78 | content: z.reference("Pet", Pet), 79 | }), 80 | }), 81 | z.response({ 82 | status: "default", 83 | description: "unexpected error", 84 | body: z.body({ 85 | type: "application/json", 86 | content: z.reference("Error", Error), 87 | }), 88 | }), 89 | ], 90 | }), 91 | 92 | z.endpoint({ 93 | name: "createPets" as const, 94 | summary: "Create a pet", 95 | tags: [z.literal("pets")], 96 | method: "POST", 97 | path: [z.literal("pets")], 98 | headers: { 99 | accept: z.parameter(z.literal("application/json")), 100 | }, 101 | responses: [ 102 | z.response({ 103 | status: 201, 104 | description: "Null response", 105 | }), 106 | z.response({ 107 | status: "default", 108 | description: "unexpected error", 109 | body: z.body({ 110 | type: "application/json", 111 | content: z.reference("Error", Error), 112 | }), 113 | }), 114 | ], 115 | }), 116 | ]); 117 | 118 | const server = { url: "http://petstore.swagger.io/v1" }; 119 | const api = openApi( 120 | schema, 121 | { version: "1.0.0", title: "Swagger Petstore", license: { name: "MIT" } }, 122 | [server] 123 | ); 124 | 125 | test("compare open api schema", () => { 126 | function compare(actual: unknown, expected: unknown) { 127 | const value = JSON.parse(JSON.stringify(actual)); 128 | expect(value).toEqual(expected); 129 | } 130 | 131 | compare(api.paths["/pets"].get, petApi.paths["/pets"].get); 132 | compare(api.paths["/pets/{petId}"].get, petApi.paths["/pets/{petId}"].get); 133 | compare(api.paths["/pets"].post, petApi.paths["/pets"].post); 134 | compare(api.components?.schemas?.Error, petApi.components?.schemas?.Error); 135 | compare(api.components?.schemas?.Pet, petApi.components?.schemas?.Pet); 136 | compare(api.components?.schemas?.Pets, petApi.components?.schemas?.Pets); 137 | compare(api, petApi); 138 | }); 139 | 140 | test("validate example request", () => { 141 | type Input = z.input; 142 | 143 | const listPets: Input = { 144 | path: ["pets"], 145 | method: "GET", 146 | query: { 147 | limit: 10, 148 | }, 149 | headers: {}, 150 | responses: { 151 | description: "A paged array of pets", 152 | status: 200, 153 | headers: { 154 | "x-next": "?", 155 | }, 156 | body: { 157 | type: "application/json", 158 | content: [ 159 | { 160 | id: 1, 161 | name: "Bello", 162 | tag: "DOG", 163 | }, 164 | ], 165 | }, 166 | }, 167 | }; 168 | expect(schema.parse(listPets).name).toEqual("listPets"); 169 | 170 | const showPetById: Input = { 171 | path: ["pets", "b945f0a8-022d-11eb-adc1-0242ac120002"], 172 | method: "GET", 173 | query: {}, 174 | headers: {}, 175 | responses: { 176 | description: "unexpected error", 177 | status: "default", 178 | headers: undefined, 179 | body: { 180 | type: "application/json", 181 | content: { 182 | code: 50, 183 | message: "This is an error", 184 | }, 185 | }, 186 | }, 187 | }; 188 | const res = schema.parse(showPetById); 189 | expect(res.name).toEqual("showPetById"); 190 | }); 191 | 192 | test("api interface", () => { 193 | const api: z.Api = { 194 | listPets: () => 195 | Promise.resolve({ 196 | status: 200, 197 | headers: { "x-next": "abc" }, 198 | body: { type: "application/json", content: [] }, 199 | }), 200 | showPetById: () => 201 | Promise.resolve({ 202 | status: 200, 203 | body: { type: "application/json", content: { id: 1, name: "Pet" } }, 204 | }), 205 | createPets: () => Promise.resolve({ status: 201 }), 206 | }; 207 | 208 | expect(api).toBeTruthy(); 209 | }); 210 | -------------------------------------------------------------------------------- /deno/lib/__tests__/pets_raw.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import petApi from "../data/petstore.ts"; 6 | import * as z from "../index.ts"; 7 | import { openApi } from "../openapi.ts"; 8 | 9 | const Error = z.object({ 10 | code: z.integer(), 11 | message: z.string(), 12 | }); 13 | 14 | const Pet = z.object({ 15 | id: z.integer("int64"), 16 | name: z.string(), 17 | tag: z.string().optional(), 18 | }); 19 | 20 | const Pets = z.array(z.reference("Pet", Pet)); 21 | 22 | const schema = z.union([ 23 | z.object({ 24 | name: z.literal("listPets").default("listPets"), 25 | summary: z.literal("List all pets").default("List all pets"), 26 | tags: z.tuple([z.literal("pets")]).default(["pets"]), 27 | path: z.tuple([z.literal("pets")]), 28 | method: z.literal("GET"), 29 | query: z.object({ 30 | limit: z 31 | .parameter(z.integer("int32").max(100).optional()) 32 | .description("How many items to return at one time (max 100)"), 33 | }), 34 | headers: z.object({}), 35 | body: z.undefined(), 36 | responses: z.union([ 37 | z.object({ 38 | status: z.literal(200), 39 | description: z.literal("A paged array of pets"), 40 | headers: z.object({ 41 | "x-next": z 42 | .parameter(z.string()) 43 | .name("x-next") 44 | .description("A link to the next page of responses"), 45 | }), 46 | body: z.object({ 47 | type: z.literal("application/json"), 48 | content: z.reference("Pets", Pets), 49 | }), 50 | }), 51 | z.object({ 52 | status: z.literal("default"), 53 | description: z.literal("unexpected error"), 54 | headers: z.object({}), 55 | body: z.object({ 56 | type: z.literal("application/json"), 57 | content: z.reference("Error", Error), 58 | }), 59 | }), 60 | ]), 61 | }), 62 | z.object({ 63 | name: z.literal("showPetById").default("showPetById"), 64 | summary: z 65 | .literal("Info for a specific pet") 66 | .default("Info for a specific pet"), 67 | tags: z.tuple([z.literal("pets")]).default(["pets"]), 68 | path: z.tuple([ 69 | z.literal("pets"), 70 | z 71 | .parameter(z.string().uuid()) 72 | .name("petId") 73 | .description("The id of the pet to retrieve"), 74 | ]), 75 | method: z.literal("GET"), 76 | query: z.object({}), 77 | headers: z.object({}), 78 | body: z.undefined(), 79 | responses: z.union([ 80 | z.object({ 81 | status: z.literal(200), 82 | description: z.literal("Expected response to a valid request"), 83 | headers: z.object({}), 84 | body: z.object({ 85 | type: z.literal("application/json"), 86 | content: z.reference("Pet", Pet), 87 | }), 88 | }), 89 | z.object({ 90 | status: z.literal("default"), 91 | description: z.literal("unexpected error"), 92 | headers: z.object({}), 93 | body: z.object({ 94 | type: z.literal("application/json"), 95 | content: z.reference("Error", Error), 96 | }), 97 | }), 98 | ]), 99 | }), 100 | z.object({ 101 | name: z.literal("createPets").default("createPets"), 102 | summary: z.literal("Create a pet").default("Create a pet"), 103 | tags: z.tuple([z.literal("pets")]).default(["pets"]), 104 | path: z.tuple([z.literal("pets")]), 105 | method: z.literal("POST"), 106 | query: z.object({}), 107 | headers: z.object({ 108 | accept: z.parameter(z.literal("application/json")), 109 | }), 110 | body: z.undefined(), 111 | responses: z.union([ 112 | z.object({ 113 | status: z.literal(201), 114 | description: z.literal("Null response"), 115 | headers: z.object({}), 116 | body: z.undefined(), 117 | }), 118 | z.object({ 119 | status: z.literal("default"), 120 | description: z.literal("unexpected error"), 121 | headers: z.object({}), 122 | body: z.object({ 123 | type: z.literal("application/json"), 124 | content: z.reference("Error", Error), 125 | }), 126 | }), 127 | ]), 128 | }), 129 | ]); 130 | 131 | test("api interface", () => { 132 | const api: z.Api = { 133 | listPets: () => 134 | Promise.resolve({ 135 | status: 200, 136 | headers: { "x-next": "abc" }, 137 | body: { type: "application/json", content: [] }, 138 | }), 139 | showPetById: () => 140 | Promise.resolve({ 141 | status: 200, 142 | headers: {}, 143 | body: { type: "application/json", content: { id: 1, name: "Pet" } }, 144 | }), 145 | createPets: () => Promise.resolve({ status: 201, headers: {} }), 146 | }; 147 | 148 | expect(api).toBeTruthy(); 149 | }); 150 | 151 | test("client interface", async () => { 152 | // @ts-ignore 153 | const client: z.Client = (req) => { 154 | const match = z.matchRequest(schema, req); 155 | return Promise.resolve({ 156 | status: 200, 157 | headers: { 158 | "x-next": "xxx", 159 | }, 160 | body: { 161 | type: "application/json", 162 | content: [ 163 | { 164 | id: 123, 165 | name: match?.shape.name.parse(undefined) ?? "", 166 | }, 167 | ], 168 | }, 169 | } as const); 170 | }; 171 | 172 | const res = await client({ 173 | method: "GET", 174 | path: ["pets"], 175 | headers: {}, 176 | query: {}, 177 | body: undefined, 178 | }); 179 | 180 | expect(res.body).toEqual({ 181 | type: "application/json", 182 | content: [{ id: 123, name: "listPets" }], 183 | }); 184 | }); 185 | 186 | test("compare open api schema", async () => { 187 | const server = { url: "http://petstore.swagger.io/v1" }; 188 | const api = openApi( 189 | schema, 190 | { 191 | version: "1.0.0", 192 | title: "Swagger Petstore", 193 | license: { name: "MIT" }, 194 | }, 195 | [server] 196 | ); 197 | 198 | function compare(actual: unknown, expected: unknown) { 199 | const value = JSON.parse(JSON.stringify(actual)); 200 | expect(value).toEqual(expected); 201 | } 202 | 203 | compare(api.paths["/pets"].get, petApi.paths["/pets"].get); 204 | compare(api.paths["/pets/{petId}"].get, petApi.paths["/pets/{petId}"].get); 205 | compare(api.paths["/pets"].post, petApi.paths["/pets"].post); 206 | compare(api.components?.schemas?.Error, petApi.components?.schemas?.Error); 207 | compare(api.components?.schemas?.Pet, petApi.components?.schemas?.Pet); 208 | compare(api.components?.schemas?.Pets, petApi.components?.schemas?.Pets); 209 | compare(api, petApi); 210 | }); 211 | 212 | test("validate example request", () => { 213 | type Input = z.input; 214 | 215 | const listPets: Input = { 216 | path: ["pets"], 217 | method: "GET", 218 | query: { 219 | limit: 10, 220 | }, 221 | headers: {}, 222 | 223 | responses: { 224 | status: 200, 225 | description: "A paged array of pets", 226 | headers: { 227 | "x-next": "?", 228 | }, 229 | body: { 230 | type: "application/json", 231 | content: [ 232 | { 233 | id: 1, 234 | name: "Bello", 235 | tag: "DOG", 236 | }, 237 | ], 238 | }, 239 | }, 240 | }; 241 | expect(schema.parse(listPets).name).toEqual("listPets"); 242 | 243 | const showPetById: Input = { 244 | path: ["pets", "b945f0a8-022d-11eb-adc1-0242ac120002"], 245 | method: "GET", 246 | query: {}, 247 | headers: {}, 248 | responses: { 249 | status: "default", 250 | description: "unexpected error", 251 | headers: {}, 252 | body: { 253 | type: "application/json", 254 | content: { 255 | code: 50, 256 | message: "This is an error", 257 | }, 258 | }, 259 | }, 260 | }; 261 | expect(schema.parse(showPetById).name).toEqual("showPetById"); 262 | }); 263 | -------------------------------------------------------------------------------- /deno/lib/__tests__/project.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | import { component, parameter } from "../index.ts"; 7 | import { openApi } from "../openapi.ts"; 8 | import { OpenAPIObject } from "../utils/openapi3-ts/OpenApi.ts"; 9 | 10 | test("test project", () => { 11 | const route: z.Http = { 12 | name: z.literal("GET_USER").default("GET_USER"), 13 | method: z.literal("GET"), 14 | path: z.tuple([z.literal("user")]), 15 | summary: z.undefined(), 16 | tags: z.tuple([z.literal("a"), z.literal("b")]).default(["a", "b"]), 17 | query: z.object({ 18 | test: parameter(z.number().max(100).optional()).description( 19 | "How many items to return at one time (max 100)" 20 | ), 21 | }), 22 | headers: z.object({}), 23 | body: z.undefined(), 24 | responses: z.union([ 25 | z.object({ 26 | description: z.literal("List of projects"), 27 | status: z.literal(200), 28 | headers: z.object({}), 29 | body: z.object({ 30 | type: z.literal("application/json"), 31 | content: component( 32 | z.object({ 33 | uuid: z.string().uuid(), 34 | name: z.string(), 35 | }) 36 | ), 37 | }), 38 | }), 39 | z.object({ 40 | description: z.literal("Not projects found"), 41 | status: z.literal(404), 42 | headers: z.object({}), 43 | body: z.undefined(), 44 | }), 45 | ]), 46 | }; 47 | 48 | const res = openApi(z.object(route)); 49 | 50 | const exp: OpenAPIObject = { 51 | openapi: "3.0.0", 52 | info: { 53 | version: "1.0.0", 54 | title: "No title", 55 | }, 56 | servers: [], 57 | paths: { 58 | "/user": { 59 | get: { 60 | operationId: "GET_USER", 61 | parameters: [ 62 | { 63 | description: "How many items to return at one time (max 100)", 64 | in: "query", 65 | name: "test", 66 | required: false, 67 | schema: { 68 | format: "int32", 69 | type: "integer", 70 | }, 71 | }, 72 | ], 73 | summary: undefined, 74 | tags: ["a", "b"], 75 | requestBody: undefined, 76 | responses: { 77 | "200": { 78 | description: "List of projects", 79 | headers: undefined, 80 | content: { 81 | "application/json": { 82 | schema: { 83 | properties: { 84 | name: { 85 | type: "string", 86 | }, 87 | uuid: { 88 | type: "string", 89 | }, 90 | }, 91 | required: ["uuid", "name"], 92 | type: "object", 93 | }, 94 | }, 95 | }, 96 | }, 97 | "404": { 98 | description: "Not projects found", 99 | headers: undefined, 100 | content: undefined, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | components: undefined, 107 | }; 108 | expect(res).toEqual(exp); 109 | }); 110 | -------------------------------------------------------------------------------- /deno/lib/__tests__/router_body.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | import { openApi } from "../openapi.ts"; 7 | 8 | test("router with body", async () => { 9 | const pet = z.reference( 10 | "Pets", 11 | z.object({ 12 | id: z.integer("int64"), 13 | name: z.string(), 14 | tag: z.string().optional(), 15 | }) 16 | ); 17 | 18 | const schema = z.endpoints([ 19 | z.endpoint({ 20 | name: "C", 21 | method: "POST", 22 | path: [z.literal("pets")], 23 | body: z.body({ 24 | type: "application/json", 25 | content: pet, 26 | }), 27 | responses: [ 28 | z.response({ 29 | status: 201, 30 | headers: {}, 31 | description: "Post with body", 32 | }), 33 | ], 34 | }), 35 | ]); 36 | 37 | const api: z.Api = { 38 | C: () => 39 | Promise.resolve({ 40 | status: 201, 41 | headers: {}, 42 | }), 43 | }; 44 | 45 | const res = await api["C"]({ 46 | method: "POST", 47 | path: ["pets"], 48 | query: {}, 49 | headers: {}, 50 | body: { type: "application/json", content: { id: 1, name: "Joe" } }, 51 | }); 52 | expect(res).toEqual({ 53 | status: 201, 54 | headers: {}, 55 | }); 56 | 57 | const open = openApi(schema); 58 | expect(open).toEqual({ 59 | components: undefined, 60 | info: { 61 | title: "No title", 62 | version: "1.0.0", 63 | }, 64 | openapi: "3.0.0", 65 | paths: { 66 | "/pets": { 67 | post: { 68 | operationId: "C", 69 | parameters: undefined, 70 | requestBody: { 71 | content: { 72 | "application/json": { 73 | schema: { 74 | $ref: "#/components/schemas/Pets", 75 | }, 76 | }, 77 | }, 78 | }, 79 | responses: { 80 | 201: { 81 | content: undefined, 82 | description: "Post with body", 83 | headers: undefined, 84 | }, 85 | }, 86 | summary: undefined, 87 | tags: undefined, 88 | }, 89 | }, 90 | }, 91 | servers: [], 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /deno/lib/__tests__/router_minimal.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | import { openApi } from "../openapi.ts"; 7 | 8 | test("minimal one endpoint", async () => { 9 | const schema = z.endpoints([ 10 | z.endpoint({ 11 | name: "A", 12 | method: "GET", 13 | responses: [ 14 | z.response({ 15 | status: 200, 16 | }), 17 | ], 18 | }), 19 | ]); 20 | 21 | const api: z.Api = { 22 | A: ({ path }) => { 23 | expect(path[0]).toEqual(""); 24 | return Promise.resolve({ 25 | status: 200, 26 | }); 27 | }, 28 | }; 29 | 30 | const res = await api["A"]({ 31 | method: "GET", 32 | path: [""], 33 | query: {}, 34 | headers: {}, 35 | }); 36 | expect(res).toEqual({ status: 200 }); 37 | }); 38 | 39 | test("minimal endpoint two endpoints", async () => { 40 | const schema = z.union([ 41 | z.endpoint({ 42 | name: "A", 43 | method: "GET", 44 | path: [z.literal("a")], 45 | responses: [ 46 | z.response({ 47 | status: 200, 48 | }), 49 | ], 50 | }), 51 | z.endpoint({ 52 | name: "B", 53 | method: "POST", 54 | path: [z.literal("b")], 55 | responses: [ 56 | z.response({ 57 | status: 200, 58 | body: [ 59 | z.body({ 60 | type: "application/json", 61 | content: z.object({ 62 | b: z.string(), 63 | }), 64 | }), 65 | z.body({ 66 | type: "plain/text", 67 | content: z.object({ 68 | c: z.string(), 69 | }), 70 | }), 71 | ], 72 | }), 73 | ], 74 | }), 75 | ]); 76 | 77 | const api: z.Api = { 78 | A: () => 79 | Promise.resolve({ 80 | status: 200, 81 | }), 82 | B: () => 83 | Promise.resolve({ 84 | status: 200, 85 | body: { type: "application/json", content: { b: "b" } }, 86 | }), 87 | }; 88 | 89 | const res = await api["B"]({ 90 | method: "POST", 91 | path: ["b"], 92 | query: {}, 93 | headers: {}, 94 | }); 95 | expect(res).toEqual({ 96 | status: 200, 97 | body: { 98 | type: "application/json", 99 | content: { b: "b" }, 100 | }, 101 | }); 102 | 103 | const open = openApi(schema); 104 | expect(open).toEqual({ 105 | components: undefined, 106 | info: { 107 | title: "No title", 108 | version: "1.0.0", 109 | }, 110 | openapi: "3.0.0", 111 | paths: { 112 | "/a": { 113 | get: { 114 | operationId: "A", 115 | parameters: undefined, 116 | requestBody: undefined, 117 | responses: { 118 | 200: { 119 | content: undefined, 120 | description: undefined, 121 | headers: undefined, 122 | }, 123 | }, 124 | summary: undefined, 125 | tags: undefined, 126 | }, 127 | }, 128 | "/b": { 129 | post: { 130 | operationId: "B", 131 | parameters: undefined, 132 | requestBody: undefined, 133 | responses: { 134 | 200: { 135 | content: { 136 | "application/json": { 137 | schema: { 138 | properties: { 139 | b: { 140 | type: "string", 141 | }, 142 | }, 143 | required: ["b"], 144 | type: "object", 145 | }, 146 | }, 147 | "plain/text": { 148 | schema: { 149 | properties: { 150 | c: { 151 | type: "string", 152 | }, 153 | }, 154 | required: ["c"], 155 | type: "object", 156 | }, 157 | }, 158 | }, 159 | description: undefined, 160 | headers: undefined, 161 | }, 162 | }, 163 | summary: undefined, 164 | tags: undefined, 165 | }, 166 | }, 167 | }, 168 | servers: [], 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /deno/lib/__tests__/type.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; 3 | const test = Deno.test; 4 | 5 | import * as z from "../index.ts"; 6 | 7 | test("type string", () => { 8 | const zod = z.string(); 9 | expect(z.createSchema(zod)).toEqual({ 10 | type: "string", 11 | }); 12 | }); 13 | 14 | test("type object", () => { 15 | const zod = z.object({ 16 | id: z.number(), 17 | name: z.string(), 18 | tag: z.string().optional(), 19 | }); 20 | expect(z.createSchema(zod)).toEqual({ 21 | type: "object", 22 | required: ["id", "name"], 23 | properties: { 24 | id: { 25 | type: "integer", 26 | format: "int32", 27 | }, 28 | name: { 29 | type: "string", 30 | }, 31 | tag: { 32 | type: "string", 33 | }, 34 | }, 35 | }); 36 | }); 37 | 38 | test("type nested object", () => { 39 | const zod = z.object({ 40 | id: z.number(), 41 | name: z.string(), 42 | obj: z 43 | .object({ 44 | test: z.string(), 45 | }) 46 | .optional(), 47 | }); 48 | expect(z.createSchema(zod)).toEqual({ 49 | type: "object", 50 | required: ["id", "name"], 51 | properties: { 52 | id: { 53 | type: "integer", 54 | format: "int32", 55 | }, 56 | name: { 57 | type: "string", 58 | }, 59 | obj: { 60 | properties: { 61 | test: { 62 | type: "string", 63 | }, 64 | }, 65 | required: ["test"], 66 | type: "object", 67 | }, 68 | }, 69 | }); 70 | }); 71 | 72 | test("type array", () => { 73 | const zod = z.array(z.string()); 74 | expect(z.createSchema(zod)).toEqual({ 75 | type: "array", 76 | items: { 77 | type: "string", 78 | }, 79 | }); 80 | }); 81 | 82 | test("type integer", () => { 83 | const int32 = z.integer(); 84 | expect(z.createSchema(int32)).toEqual({ 85 | format: "int32", 86 | type: "integer", 87 | }); 88 | 89 | const int64 = z.integer("int64").max(100); 90 | expect(z.createSchema(int64)).toEqual({ 91 | format: "int64", 92 | type: "integer", 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /deno/lib/api.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | import { HttpSchema } from "./model.ts"; 3 | import { PickUnion } from "./utils.ts"; 4 | 5 | export type ApiNames = z.output extends { 6 | name: string; 7 | } 8 | ? z.output["name"] 9 | : never; 10 | 11 | export type ApiRequestAttributes = 12 | | "method" 13 | | "path" 14 | | "query" 15 | | "headers" 16 | | "body"; 17 | export type ApiResponseAttributes = "status" | "headers" | "body"; 18 | export type ApiRequest = Extract< 19 | z.output, 20 | { name: Key } 21 | >; 22 | export type ApiResponse = ApiRequest< 23 | T, 24 | Key 25 | >["responses"]; 26 | export type ApiFunction = ( 27 | request: Readonly, ApiRequestAttributes>> 28 | ) => Readonly< 29 | Promise< 30 | ApiResponseAttributes extends keyof ApiResponse 31 | ? PickUnion, ApiResponseAttributes> 32 | : undefined 33 | > 34 | >; 35 | 36 | export type Api = { 37 | [Key in ApiNames]: ApiFunction; 38 | }; 39 | 40 | export type ApiFragment> = Pick< 41 | Api, 42 | Key 43 | >; 44 | -------------------------------------------------------------------------------- /deno/lib/client.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | import { HttpSchema } from "./model.ts"; 3 | import { PickUnion } from "./utils.ts"; 4 | 5 | type ClientRequestAttributes = "method" | "path" | "query" | "headers" | "body"; 6 | type ClientResponseAttributes = "status" | "headers" | "body"; 7 | type ClientRequest = PickUnion< 8 | z.output, 9 | ClientRequestAttributes 10 | >; 11 | type ClientRequestResponses = PickUnion< 12 | z.output, 13 | ClientRequestAttributes | "responses" 14 | >; 15 | type ClientMapper< 16 | T extends HttpSchema, 17 | R extends ClientRequest 18 | > = NonNullable<{ 19 | method: R["method"]; 20 | path: R["path"]; 21 | query: R["query"]; 22 | headers: R["headers"]; 23 | body?: R["body"]; 24 | }>; 25 | type ClientMatch> = Extract< 26 | ClientRequestResponses, 27 | ClientMapper 28 | >["responses"]; 29 | 30 | export type Client = >( 31 | req: R 32 | ) => Promise< 33 | ClientResponseAttributes extends keyof ClientMatch 34 | ? PickUnion, ClientResponseAttributes> 35 | : undefined 36 | >; 37 | -------------------------------------------------------------------------------- /deno/lib/component.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | 3 | export class Component extends z.ZodType< 4 | T["_output"], 5 | T["_def"], 6 | T["_input"] 7 | > { 8 | _parse( 9 | _ctx: z.ParseContext, 10 | _data: any, 11 | _parsedType: z.ZodParsedType 12 | ): z.ParseReturnType { 13 | return this.component._parse(_ctx, _data, _parsedType); 14 | } 15 | readonly component: z.ZodTypeAny; 16 | 17 | constructor(type: z.ZodTypeAny) { 18 | super(type._def); 19 | this.component = type; 20 | } 21 | 22 | public toJSON = () => this._def; 23 | 24 | static create(type: z.ZodTypeAny) { 25 | return new Component(type); 26 | } 27 | } 28 | 29 | export const component = Component.create; 30 | -------------------------------------------------------------------------------- /deno/lib/data/petstore.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | openapi: "3.0.0", 3 | info: { 4 | version: "1.0.0", 5 | title: "Swagger Petstore", 6 | license: { 7 | name: "MIT", 8 | }, 9 | }, 10 | servers: [ 11 | { 12 | url: "http://petstore.swagger.io/v1", 13 | }, 14 | ], 15 | paths: { 16 | "/pets": { 17 | get: { 18 | summary: "List all pets", 19 | operationId: "listPets", 20 | tags: ["pets"], 21 | parameters: [ 22 | { 23 | name: "limit", 24 | in: "query", 25 | description: "How many items to return at one time (max 100)", 26 | required: false, 27 | schema: { 28 | type: "integer", 29 | format: "int32", 30 | }, 31 | }, 32 | ], 33 | responses: { 34 | "200": { 35 | description: "A paged array of pets", 36 | headers: { 37 | "x-next": { 38 | description: "A link to the next page of responses", 39 | schema: { 40 | type: "string", 41 | }, 42 | }, 43 | }, 44 | content: { 45 | "application/json": { 46 | schema: { 47 | $ref: "#/components/schemas/Pets", 48 | }, 49 | }, 50 | }, 51 | }, 52 | default: { 53 | description: "unexpected error", 54 | content: { 55 | "application/json": { 56 | schema: { 57 | $ref: "#/components/schemas/Error", 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | post: { 65 | summary: "Create a pet", 66 | operationId: "createPets", 67 | tags: ["pets"], 68 | responses: { 69 | "201": { 70 | description: "Null response", 71 | }, 72 | default: { 73 | description: "unexpected error", 74 | content: { 75 | "application/json": { 76 | schema: { 77 | $ref: "#/components/schemas/Error", 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | "/pets/{petId}": { 86 | get: { 87 | summary: "Info for a specific pet", 88 | operationId: "showPetById", 89 | tags: ["pets"], 90 | parameters: [ 91 | { 92 | name: "petId", 93 | in: "path", 94 | required: true, 95 | description: "The id of the pet to retrieve", 96 | schema: { 97 | type: "string", 98 | }, 99 | }, 100 | ], 101 | responses: { 102 | "200": { 103 | description: "Expected response to a valid request", 104 | content: { 105 | "application/json": { 106 | schema: { 107 | $ref: "#/components/schemas/Pet", 108 | }, 109 | }, 110 | }, 111 | }, 112 | default: { 113 | description: "unexpected error", 114 | content: { 115 | "application/json": { 116 | schema: { 117 | $ref: "#/components/schemas/Error", 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | }, 126 | components: { 127 | schemas: { 128 | Pet: { 129 | type: "object", 130 | required: ["id", "name"], 131 | properties: { 132 | id: { 133 | type: "integer", 134 | format: "int64", 135 | }, 136 | name: { 137 | type: "string", 138 | }, 139 | tag: { 140 | type: "string", 141 | }, 142 | }, 143 | }, 144 | Pets: { 145 | type: "array", 146 | items: { 147 | $ref: "#/components/schemas/Pet", 148 | }, 149 | }, 150 | Error: { 151 | type: "object", 152 | required: ["code", "message"], 153 | properties: { 154 | code: { 155 | type: "integer", 156 | format: "int32", 157 | }, 158 | message: { 159 | type: "string", 160 | }, 161 | }, 162 | }, 163 | }, 164 | }, 165 | } as const; 166 | -------------------------------------------------------------------------------- /deno/lib/deps.ts: -------------------------------------------------------------------------------- 1 | export * from "https://raw.githubusercontent.com/colinhacks/zod/master/deno/lib/mod.ts"; 2 | -------------------------------------------------------------------------------- /deno/lib/dsl.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | import { 3 | Content, 4 | HttpBodyObject, 5 | HttpBodyUnion, 6 | HttpObject, 7 | HttpResponseObject, 8 | Path, 9 | } from "./model.ts"; 10 | import { Parameter } from "./parameter.ts"; 11 | 12 | export type Body = { 13 | readonly type: string; 14 | readonly content: Content; 15 | }; 16 | 17 | export type Request = { 18 | readonly name: string; 19 | readonly summary?: string; 20 | readonly tags?: [z.ZodLiteral, ...z.ZodLiteral[]] | []; 21 | readonly method: "GET" | "POST" | "PUT" | "DELETE"; 22 | readonly path?: [Path, ...Path[]]; 23 | readonly query?: { [key: string]: Parameter }; 24 | readonly headers?: { [key: string]: Parameter }; 25 | readonly body?: 26 | | [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 27 | | [HttpBodyObject] 28 | | HttpBodyObject; 29 | }; 30 | 31 | export type Response = { 32 | readonly description?: string; 33 | readonly status: number | string; 34 | readonly headers?: { [key: string]: Parameter }; 35 | readonly body?: 36 | | [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 37 | | [HttpBodyObject] 38 | | HttpBodyObject; 39 | }; 40 | 41 | export type PathInput = [string | Path, ...(string | Path)[]]; 42 | export type PathMapper = { 43 | [Index in keyof Tuple]: Tuple[Index] extends string 44 | ? z.ZodLiteral 45 | : Tuple[Index]; 46 | }; 47 | export function path(...input: T): PathMapper { 48 | // @ts-ignore 49 | return input.map((it) => (typeof it === "string" ? z.literal(it) : it)); 50 | } 51 | 52 | export type Endpoint = Request & { 53 | responses?: 54 | | [HttpResponseObject, HttpResponseObject, ...HttpResponseObject[]] 55 | | [HttpResponseObject]; 56 | }; 57 | 58 | export function endpoints(types: [T]): T; 59 | export function endpoints< 60 | T1 extends HttpObject, 61 | T2 extends HttpObject, 62 | T3 extends HttpObject 63 | >(types: [T1, T2, ...T3[]]): z.ZodUnion<[T1, T2, ...T3[]]>; 64 | export function endpoints( 65 | types: [T] | [T, T, ...T[]] 66 | ): T | z.ZodUnion<[T, T, ...T[]]> { 67 | // @ts-ignore 68 | return types.length === 1 ? types[0] : z.union<[T, T, ...T[]]>(types); 69 | } 70 | 71 | export type EndpointMapper = z.ZodObject<{ 72 | name: z.ZodDefault>; 73 | summary: T["summary"] extends string 74 | ? z.ZodDefault> 75 | : z.ZodUndefined; 76 | tags: T["tags"] extends [z.ZodTypeAny, ...z.ZodTypeAny[]] | [] 77 | ? z.ZodDefault> 78 | : z.ZodUndefined; 79 | method: z.ZodLiteral; 80 | path: T["path"] extends [Path, ...Path[]] 81 | ? z.ZodTuple 82 | : z.ZodTuple<[z.ZodLiteral<"">]>; 83 | query: T["query"] extends z.ZodRawShape 84 | ? z.ZodObject 85 | : z.ZodObject<{}>; 86 | headers: T["headers"] extends z.ZodRawShape 87 | ? z.ZodObject 88 | : z.ZodObject<{}>; 89 | body: T["body"] extends [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 90 | ? z.ZodUnion 91 | : T["body"] extends [HttpBodyObject] 92 | ? T["body"][0] 93 | : T["body"] extends HttpBodyObject 94 | ? T["body"] 95 | : z.ZodUndefined; 96 | responses: T["responses"] extends [ 97 | HttpResponseObject, 98 | HttpResponseObject, 99 | ...HttpResponseObject[] 100 | ] 101 | ? z.ZodUnion 102 | : T["responses"] extends [HttpResponseObject] 103 | ? T["responses"][0] 104 | : z.ZodUndefined; 105 | }>; 106 | 107 | export function endpoint( 108 | endpoint: Readonly 109 | ): EndpointMapper { 110 | // @ts-ignore 111 | return z.object({ 112 | // @ts-ignore 113 | name: z.literal(endpoint.name).default(endpoint.name), 114 | summary: 115 | endpoint.summary !== undefined 116 | ? // @ts-ignore 117 | z.literal(endpoint.summary).default(endpoint.summary) 118 | : z.undefined(), 119 | tags: 120 | endpoint.tags !== undefined 121 | ? // @ts-ignore 122 | z.tuple(endpoint.tags).default(endpoint.tags.map((_) => _._def.value)) 123 | : z.undefined(), 124 | method: z.literal(endpoint.method), 125 | // @ts-ignore 126 | path: endpoint.path !== undefined ? z.tuple(endpoint.path) : [], 127 | query: 128 | endpoint.query !== undefined 129 | ? z.object(endpoint.query as z.ZodRawShape) 130 | : z.object({}), 131 | headers: 132 | endpoint.headers !== undefined 133 | ? z.object(endpoint.headers as z.ZodRawShape) 134 | : z.object({}), 135 | // @ts-ignore 136 | body: transformBody(endpoint.body), 137 | // @ts-ignore 138 | responses: 139 | endpoint.responses !== undefined 140 | ? // @ts-ignore 141 | z.union(endpoint.responses) 142 | : z.undefined(), 143 | }); 144 | } 145 | 146 | export type BodyMapper = z.ZodObject<{ 147 | type: z.ZodLiteral; 148 | content: T["content"]; 149 | }>; 150 | 151 | export function body(body: Readonly): BodyMapper { 152 | return z.object({ 153 | type: z.literal(body.type), 154 | content: body.content, 155 | }); 156 | } 157 | 158 | export type ResponseMapper = z.ZodObject<{ 159 | description: T["description"] extends string 160 | ? z.ZodLiteral 161 | : z.ZodUndefined; 162 | status: z.ZodLiteral; 163 | headers: T["headers"] extends z.ZodRawShape 164 | ? z.ZodObject 165 | : z.ZodUndefined; 166 | body: T["body"] extends [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 167 | ? z.ZodUnion 168 | : T["body"] extends [HttpBodyObject] 169 | ? T["body"][0] 170 | : T["body"] extends HttpBodyObject 171 | ? T["body"] 172 | : z.ZodUndefined; 173 | }>; 174 | 175 | export function response( 176 | response: Readonly 177 | ): ResponseMapper { 178 | // @ts-ignore 179 | return z.object({ 180 | status: z.literal(response.status), 181 | description: z.literal(response.description), 182 | headers: 183 | response.headers !== undefined 184 | ? z.object(response.headers as z.ZodRawShape) 185 | : z.undefined(), 186 | body: transformBody(response.body), 187 | }); 188 | } 189 | 190 | function transformBody( 191 | body?: 192 | | [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 193 | | [HttpBodyObject] 194 | | HttpBodyObject 195 | ): HttpBodyUnion { 196 | if (body === undefined) { 197 | return z.undefined(); 198 | } 199 | if (Array.isArray(body)) { 200 | if (body.length === 1) { 201 | return body[0]; 202 | } 203 | // @ts-ignore 204 | return z.union(body); 205 | } 206 | return body; 207 | } 208 | -------------------------------------------------------------------------------- /deno/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api.ts"; 2 | export * from "./client.ts"; 3 | export { Component, component } from "./component.ts"; 4 | export * from "./deps.ts"; 5 | export * from "./dsl.ts"; 6 | export { Integer, integer } from "./integer.ts"; 7 | export * from "./match.ts"; 8 | export * from "./model.ts"; 9 | export { createSchema, openApi } from "./openapi.ts"; 10 | export { Parameter, parameter } from "./parameter.ts"; 11 | export { Reference, reference } from "./reference.ts"; 12 | -------------------------------------------------------------------------------- /deno/lib/integer.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | 3 | export interface IntegerDef extends z.ZodNumberDef { 4 | format: string; 5 | } 6 | 7 | export class Integer extends z.ZodNumber { 8 | public toJSON = () => this._def; 9 | 10 | constructor(def: IntegerDef) { 11 | super(def); 12 | } 13 | static create = (format?: string): Integer => { 14 | return new Integer({ 15 | checks: [], 16 | typeName: z.ZodFirstPartyTypeKind.ZodNumber, 17 | format: format ?? "int32", 18 | }); 19 | }; 20 | } 21 | 22 | export const integer = Integer.create; 23 | -------------------------------------------------------------------------------- /deno/lib/match.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | import { 3 | HttpObject, 4 | HttpRequestObject, 5 | HttpResponseObject, 6 | HttpResponseUnion, 7 | HttpUnion, 8 | } from "./model.ts"; 9 | 10 | export type MatchRequest = Pick< 11 | z.output, 12 | "method" | "path" | "query" | "headers" | "body" 13 | >; 14 | export type MatchResponse = Pick< 15 | z.output, 16 | "status" | "headers" | "body" 17 | >; 18 | 19 | const requestPicker = { 20 | method: true, 21 | path: true, 22 | query: true, 23 | headers: true, 24 | body: true, 25 | } as const; 26 | const responsePicker = { status: true, headers: true, body: true } as const; 27 | 28 | export function matchRequest( 29 | schema: HttpUnion, 30 | req: MatchRequest 31 | ): HttpObject | undefined { 32 | function check(request: HttpRequestObject) { 33 | try { 34 | request.pick(requestPicker).parse(req); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | return schema.options.find(check); 42 | } 43 | 44 | export function matchResponse( 45 | responses: HttpResponseUnion, 46 | res: MatchResponse 47 | ): HttpResponseObject | undefined { 48 | function check(response: HttpResponseObject) { 49 | try { 50 | response.pick(responsePicker).parse(res); 51 | return true; 52 | } catch (e) { 53 | return false; 54 | } 55 | } 56 | if ("shape" in responses) { 57 | return responses; 58 | } 59 | if ("options" in responses) { 60 | return responses.options.find(check); 61 | } 62 | return undefined; 63 | } 64 | -------------------------------------------------------------------------------- /deno/lib/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./index.ts"; 2 | -------------------------------------------------------------------------------- /deno/lib/model.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component.ts"; 2 | import * as z from "./deps.ts"; 3 | import { Parameter } from "./parameter.ts"; 4 | import { Reference } from "./reference.ts"; 5 | 6 | export type Path = 7 | | z.ZodLiteral 8 | | z.ZodString 9 | | z.ZodNumber 10 | | z.ZodBoolean 11 | | Parameter; 12 | export type ParameterObject = z.ZodObject<{ [key: string]: Parameter }>; 13 | export type Content = 14 | | Reference 15 | | Component 16 | | z.ZodFirstPartySchemaTypes; 17 | 18 | export type HttpBody = { 19 | type: z.ZodLiteral; 20 | content: Content; 21 | }; 22 | export type HttpBodyObject = z.ZodObject; 23 | export type HttpBodyUnion = 24 | | HttpBodyObject 25 | | z.ZodUnion<[HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]]> 26 | | z.ZodUndefined; 27 | 28 | export type HttpRequest = { 29 | name: z.ZodDefault> | z.ZodUndefined; 30 | method: z.ZodLiteral; 31 | path: z.ZodTuple<[Path, ...Path[]]> | z.ZodUndefined; 32 | summary: z.ZodDefault> | z.ZodUndefined; 33 | tags: z.ZodDefault> | z.ZodUndefined; 34 | query: ParameterObject; 35 | headers: ParameterObject; 36 | body: HttpBodyUnion; 37 | }; 38 | 39 | export type HttpRequestObject = z.ZodObject; 40 | 41 | export type HttpResponse = { 42 | status: z.ZodLiteral; 43 | description: z.ZodLiteral | z.ZodUndefined; 44 | headers: ParameterObject | z.ZodUndefined; 45 | body: HttpBodyUnion; 46 | }; 47 | 48 | export type HttpResponseObject = z.ZodObject; 49 | 50 | export type HttpResponseUnion = 51 | | HttpResponseObject 52 | | z.ZodUnion< 53 | [HttpResponseObject, HttpResponseObject, ...HttpResponseObject[]] 54 | > 55 | | z.ZodUndefined; 56 | 57 | export type Http = HttpRequest & { 58 | responses: HttpResponseUnion; 59 | }; 60 | 61 | export type HttpObject = z.ZodObject; 62 | export type HttpOptions = [HttpObject, HttpObject, ...HttpObject[]]; 63 | export type HttpUnion = z.ZodUnion; 64 | export type HttpSchema = HttpObject | HttpUnion; 65 | -------------------------------------------------------------------------------- /deno/lib/openapi.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component.ts"; 2 | import { ZodFirstPartyTypeKind, ZodTypeAny } from "./deps.ts"; 3 | import { 4 | HttpBodyUnion, 5 | HttpObject, 6 | HttpResponseObject, 7 | HttpResponseUnion, 8 | HttpSchema, 9 | ParameterObject as ParameterObj, 10 | Path, 11 | } from "./model.ts"; 12 | import { Parameter } from "./parameter.ts"; 13 | import { Reference } from "./reference.ts"; 14 | import { 15 | ComponentsObject, 16 | ContentObject, 17 | HeadersObject, 18 | InfoObject, 19 | OpenAPIObject, 20 | OperationObject, 21 | ParameterObject, 22 | PathsObject, 23 | ReferenceObject, 24 | RequestBodyObject, 25 | ResponseObject, 26 | ResponsesObject, 27 | SchemaObject, 28 | ServerObject, 29 | } from "./utils/openapi3-ts/OpenApi.ts"; 30 | 31 | const base = { 32 | openapi: "3.0.0", 33 | }; 34 | 35 | function mapSchema(type: ZodTypeAny): SchemaObject | undefined { 36 | switch (type._def.typeName) { 37 | case ZodFirstPartyTypeKind.ZodNumber: 38 | if ("format" in type._def) { 39 | return { 40 | type: "integer", 41 | format: type._def.format, 42 | }; 43 | } 44 | return { 45 | type: "integer", 46 | format: "int32", 47 | }; 48 | case ZodFirstPartyTypeKind.ZodBigInt: 49 | return { 50 | type: "integer", 51 | format: "int32", 52 | }; 53 | case ZodFirstPartyTypeKind.ZodString: 54 | return { 55 | type: "string", 56 | }; 57 | default: 58 | return undefined; 59 | } 60 | } 61 | 62 | export function createSchema( 63 | obj: ZodTypeAny | Reference | Component 64 | ): SchemaObject | ReferenceObject | undefined { 65 | if ("reference" in obj) { 66 | return { $ref: `#/components/schemas/${obj.state.name}` }; 67 | } 68 | if ("component" in obj) { 69 | return createSchema(obj.component); 70 | } 71 | if ("innerType" in obj._def) { 72 | return createSchema(obj._def.innerType); 73 | } 74 | if ("type" in obj._def) { 75 | return { 76 | type: "array", 77 | items: createSchema(obj._def.type), 78 | }; 79 | } 80 | if ("shape" in obj._def) { 81 | const shape = obj._def.shape(); 82 | return { 83 | type: "object", 84 | properties: Object.keys(shape).reduce((acc, cur) => { 85 | return { 86 | ...acc, 87 | [cur]: createSchema(shape[cur]), 88 | }; 89 | }, {}), 90 | required: Object.keys(shape).reduce((acc, cur) => { 91 | if (shape[cur]._def.typeName !== ZodFirstPartyTypeKind.ZodOptional) { 92 | return [...acc, cur]; 93 | } 94 | return acc; 95 | }, []), 96 | }; 97 | } 98 | if ("checks" in obj._def) { 99 | return mapSchema(obj); 100 | } 101 | return undefined; 102 | } 103 | 104 | export function openApi( 105 | schema: HttpSchema, 106 | info: InfoObject = { title: "No title", version: "1.0.0" }, 107 | servers: ServerObject[] = [] 108 | ): OpenAPIObject { 109 | const options = "options" in schema ? schema.options : [schema]; 110 | const paths = createPaths(options); 111 | const components = createComponents(options); 112 | return { ...base, info, servers, paths, components }; 113 | } 114 | 115 | function createPaths(options: HttpObject[]): PathsObject { 116 | return options.reduce((acc, cur) => { 117 | const shape = cur._def.shape(); 118 | const method = shape.method._def.value; 119 | const path = 120 | shape.path._def !== undefined && "items" in shape.path._def 121 | ? "/" + 122 | shape.path._def.items 123 | .map((p, index) => { 124 | if ("state" in p) { 125 | return `{${p.state.name}}`; 126 | } 127 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodLiteral) { 128 | return p._def.value; 129 | } 130 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodString) { 131 | return `{param_${index}}`; 132 | } 133 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodNumber) { 134 | return `{param_${index}}`; 135 | } 136 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodBoolean) { 137 | return `{param_${index}}`; 138 | } 139 | }) 140 | .join("/") 141 | : "/"; 142 | return { 143 | ...acc, 144 | [path]: { 145 | ...acc[path], 146 | [method.toLowerCase()]: createOperationObject(cur), 147 | }, 148 | }; 149 | }, {}); 150 | } 151 | 152 | function createComponents(options: HttpObject[]): ComponentsObject | undefined { 153 | const schemas = options 154 | .flatMap((http) => { 155 | return mapResponsesObject(http.shape.responses); 156 | }) 157 | .map((response) => { 158 | const body = response.shape.body; 159 | if ("shape" in body) { 160 | return body.shape.content; 161 | } 162 | if ("options" in body) { 163 | body.options.map((it) => it.shape.content); 164 | } 165 | return undefined; 166 | }) 167 | .reduce((acc, cur) => { 168 | if ( 169 | cur != null && 170 | "reference" in cur && 171 | "state" in cur && 172 | cur.state.name 173 | ) { 174 | return { ...acc, [cur.state.name]: createSchema(cur.reference) }; 175 | } else { 176 | return acc; 177 | } 178 | }, {}); 179 | return Object.keys(schemas).length > 0 180 | ? { 181 | schemas: schemas, 182 | } 183 | : undefined; 184 | } 185 | 186 | function createOperationObject(http: HttpObject): OperationObject { 187 | const shape = http._def.shape(); 188 | return { 189 | summary: 190 | "defaultValue" in shape.summary._def 191 | ? shape.summary._def.defaultValue() 192 | : undefined, 193 | operationId: 194 | "defaultValue" in shape.name._def 195 | ? shape.name._def.defaultValue() 196 | : undefined, 197 | tags: 198 | "defaultValue" in shape.tags._def 199 | ? (shape.tags._def.defaultValue() as string[]) 200 | : undefined, 201 | requestBody: createRequestBody(http), 202 | parameters: createParameterObject(http), 203 | responses: createResponsesObject(shape.responses), 204 | }; 205 | } 206 | 207 | function createRequestBody( 208 | http: HttpObject 209 | ): RequestBodyObject | ReferenceObject | undefined { 210 | const shape = http._def.shape(); 211 | const content = createContentObject(shape.body); 212 | return content ? { content } : undefined; 213 | } 214 | 215 | function createParameterObject(http: HttpObject) { 216 | const shape = http._def.shape(); 217 | const res = [ 218 | ...("shape" in shape.query._def 219 | ? createQueryParameterObject(shape.query._def.shape()) 220 | : []), 221 | ...(shape.path._def && "items" in shape.path._def 222 | ? shape.path._def.items 223 | .map(createPathParameterObject) 224 | .filter((it) => it.schema !== undefined) 225 | : []), 226 | ]; 227 | return res.length > 0 ? res : undefined; 228 | } 229 | 230 | function createPathParameterObject(it: Path, index: number): ParameterObject { 231 | return { 232 | name: "state" in it && it.state.name ? it.state.name : `param_${index}`, 233 | in: "path", 234 | description: "state" in it ? it.state.description : undefined, 235 | required: !("innerType" in it._def), 236 | schema: mapSchema(it), 237 | }; 238 | } 239 | 240 | function createQueryParameterObject( 241 | it: Record 242 | ): ParameterObject[] { 243 | return Object.keys(it).map((key) => ({ 244 | name: key, 245 | in: "query", 246 | description: "state" in it[key] ? it[key].state.description : undefined, 247 | required: !("innerType" in it[key]._def), 248 | // @ts-ignore 249 | schema: 250 | "innerType" in it[key]._def 251 | ? // @ts-ignore 252 | mapSchema(it[key]._def.innerType) 253 | : mapSchema(it[key]), 254 | })); 255 | } 256 | 257 | function createResponsesObject(responses: HttpResponseUnion): ResponsesObject { 258 | if ("shape" in responses) { 259 | return createResponseObject(responses); 260 | } 261 | if ("options" in responses) { 262 | return responses.options.reduce((acc, cur) => { 263 | return { 264 | ...acc, 265 | ...createResponseObject(cur), 266 | }; 267 | }, {}); 268 | } 269 | return {}; 270 | } 271 | 272 | function mapResponsesObject( 273 | responses: HttpResponseUnion 274 | ): HttpResponseObject[] { 275 | if ("options" in responses) { 276 | return responses.options.map((it) => it); 277 | } 278 | if ("shape" in responses) { 279 | return [responses]; 280 | } 281 | return []; 282 | } 283 | 284 | function createResponseObject( 285 | response: HttpResponseObject 286 | ): Record { 287 | const shape = response._def.shape(); 288 | const name = shape.status._def.value as string; 289 | return { 290 | [name]: { 291 | description: 292 | "value" in shape.description._def ? shape.description._def.value : "", 293 | headers: 294 | "shape" in shape.headers 295 | ? createHeadersObject(shape.headers) 296 | : undefined, 297 | content: createContentObject(shape.body), 298 | }, 299 | }; 300 | } 301 | function createContentObject(body: HttpBodyUnion): ContentObject | undefined { 302 | if ("shape" in body) { 303 | return { 304 | [body.shape.type._def.value]: { 305 | schema: createSchema(body.shape.content), 306 | }, 307 | }; 308 | } 309 | if ("options" in body) { 310 | return body.options.reduce( 311 | (acc, cur) => ({ ...acc, ...createContentObject(cur) }), 312 | {} 313 | ); 314 | } 315 | return undefined; 316 | } 317 | 318 | function createHeadersObject(headers: ParameterObj): HeadersObject | undefined { 319 | const shape = headers._def.shape(); 320 | 321 | if (Object.keys(shape).length === 0) { 322 | return undefined; 323 | } 324 | 325 | return Object.keys(shape).reduce((acc, cur) => { 326 | const obj = shape[cur]; 327 | if ("value" in obj._def) { 328 | return { 329 | ...acc, 330 | [cur]: { 331 | schema: mapSchema(obj), 332 | }, 333 | }; 334 | } 335 | if ("state" in obj) { 336 | return { 337 | ...acc, 338 | [cur]: { 339 | description: obj.state.description, 340 | schema: mapSchema(obj), 341 | }, 342 | }; 343 | } 344 | return { 345 | ...acc, 346 | [cur]: {}, 347 | }; 348 | }, {}); 349 | } 350 | -------------------------------------------------------------------------------- /deno/lib/parameter.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | 3 | type ParameterState = { 4 | name?: string; 5 | description?: string; 6 | }; 7 | 8 | export class Parameter extends z.ZodType { 9 | readonly type: z.ZodType; 10 | state: ParameterState; 11 | 12 | _parse( 13 | _ctx: z.ParseContext, 14 | _data: any, 15 | _parsedType: z.ZodParsedType 16 | ): z.ParseReturnType { 17 | return this.type._parse(_ctx, _data, _parsedType); 18 | } 19 | constructor(type: z.ZodType) { 20 | super(type._def); 21 | this.type = type; 22 | this.state = { 23 | name: undefined, 24 | description: undefined, 25 | }; 26 | } 27 | 28 | public toJSON = () => this._def; 29 | 30 | public name(name: string) { 31 | this.state = { ...this.state, name }; 32 | return this; 33 | } 34 | 35 | public description(description: string) { 36 | this.state = { ...this.state, description }; 37 | return this; 38 | } 39 | 40 | static create(type: z.ZodType) { 41 | return new Parameter(type); 42 | } 43 | } 44 | 45 | export const parameter = Parameter.create; 46 | -------------------------------------------------------------------------------- /deno/lib/playground.ts: -------------------------------------------------------------------------------- 1 | export * from "https://raw.githubusercontent.com/colinhacks/zod/master/deno/lib/mod.ts"; 2 | 3 | const x = z.literal(""); 4 | 5 | console.log(x); 6 | -------------------------------------------------------------------------------- /deno/lib/reference.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps.ts"; 2 | 3 | export class Reference extends z.ZodType< 4 | T["_output"], 5 | T["_def"], 6 | T["_input"] 7 | > { 8 | readonly reference: z.ZodTypeAny; 9 | state: { 10 | name?: string; 11 | }; 12 | _parse( 13 | _ctx: z.ParseContext, 14 | _data: any, 15 | _parsedType: z.ZodParsedType 16 | ): z.ParseReturnType { 17 | return this.reference._parse(_ctx, _data, _parsedType); 18 | } 19 | 20 | constructor(name: string, type: T) { 21 | super(type._def); 22 | this.reference = type; 23 | this.state = { name }; 24 | } 25 | 26 | public toJSON = () => this._def; 27 | 28 | static create(name: string, type: T) { 29 | return new Reference(name, type); 30 | } 31 | } 32 | 33 | export const reference = Reference.create; 34 | -------------------------------------------------------------------------------- /deno/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export type PickUnion = T extends any 2 | ? { [P in K]: T[P] } 3 | : never; 4 | -------------------------------------------------------------------------------- /deno/lib/utils/openapi3-ts/OpenApi.ts: -------------------------------------------------------------------------------- 1 | // Typed interfaces for OpenAPI 3.0.0-RC 2 | // see https://github.com/OAI/OpenAPI-Specification/blob/3.0.0-rc0/versions/3.0.md 3 | 4 | import { 5 | ISpecificationExtension, 6 | SpecificationExtension, 7 | } from "./SpecificationExtension.ts"; 8 | 9 | export function getExtension( 10 | obj: ISpecificationExtension, 11 | extensionName: string 12 | ): any { 13 | if (SpecificationExtension.isValidExtension(extensionName)) { 14 | return obj[extensionName]; 15 | } 16 | return undefined; 17 | } 18 | export function addExtension( 19 | obj: ISpecificationExtension, 20 | extensionName: string, 21 | extension: any 22 | ): void { 23 | if (SpecificationExtension.isValidExtension(extensionName)) { 24 | obj[extensionName] = extension; 25 | } 26 | } 27 | 28 | export interface OpenAPIObject extends ISpecificationExtension { 29 | openapi: string; 30 | info: InfoObject; 31 | servers?: ServerObject[]; 32 | paths: PathsObject; 33 | components?: ComponentsObject; 34 | security?: SecurityRequirementObject[]; 35 | tags?: TagObject[]; 36 | externalDocs?: ExternalDocumentationObject; 37 | } 38 | export interface InfoObject extends ISpecificationExtension { 39 | title: string; 40 | description?: string; 41 | termsOfService?: string; 42 | contact?: ContactObject; 43 | license?: LicenseObject; 44 | version: string; 45 | } 46 | export interface ContactObject extends ISpecificationExtension { 47 | name?: string; 48 | url?: string; 49 | email?: string; 50 | } 51 | export interface LicenseObject extends ISpecificationExtension { 52 | name: string; 53 | url?: string; 54 | } 55 | export interface ServerObject extends ISpecificationExtension { 56 | url: string; 57 | description?: string; 58 | variables?: { [v: string]: ServerVariableObject }; 59 | } 60 | export interface ServerVariableObject extends ISpecificationExtension { 61 | enum?: string[] | boolean[] | number[]; 62 | default: string | boolean | number; 63 | description?: string; 64 | } 65 | export interface ComponentsObject extends ISpecificationExtension { 66 | schemas?: { [schema: string]: SchemaObject | ReferenceObject }; 67 | responses?: { [response: string]: ResponseObject | ReferenceObject }; 68 | parameters?: { [parameter: string]: ParameterObject | ReferenceObject }; 69 | examples?: { [example: string]: ExampleObject | ReferenceObject }; 70 | requestBodies?: { [request: string]: RequestBodyObject | ReferenceObject }; 71 | headers?: { [header: string]: HeaderObject | ReferenceObject }; 72 | securitySchemes?: { 73 | [securityScheme: string]: SecuritySchemeObject | ReferenceObject; 74 | }; 75 | links?: { [link: string]: LinkObject | ReferenceObject }; 76 | callbacks?: { [callback: string]: CallbackObject | ReferenceObject }; 77 | } 78 | 79 | /** 80 | * Rename it to Paths Object to be consistent with the spec 81 | * See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#pathsObject 82 | */ 83 | export interface PathsObject extends ISpecificationExtension { 84 | // [path: string]: PathItemObject; 85 | [path: string]: PathItemObject | any; // Hack for allowing ISpecificationExtension 86 | } 87 | 88 | /** 89 | * @deprecated 90 | * Create a type alias for backward compatibility 91 | */ 92 | export type PathObject = PathsObject; 93 | 94 | export function getPath( 95 | pathsObject: PathsObject, 96 | path: string 97 | ): PathItemObject | undefined { 98 | if (SpecificationExtension.isValidExtension(path)) { 99 | return undefined; 100 | } 101 | return pathsObject[path] as PathItemObject; 102 | } 103 | 104 | export interface PathItemObject extends ISpecificationExtension { 105 | $ref?: string; 106 | summary?: string; 107 | description?: string; 108 | get?: OperationObject; 109 | put?: OperationObject; 110 | post?: OperationObject; 111 | delete?: OperationObject; 112 | options?: OperationObject; 113 | head?: OperationObject; 114 | patch?: OperationObject; 115 | trace?: OperationObject; 116 | servers?: ServerObject[]; 117 | parameters?: (ParameterObject | ReferenceObject)[]; 118 | } 119 | export interface OperationObject extends ISpecificationExtension { 120 | tags?: (string | undefined)[]; 121 | summary?: string; 122 | description?: string; 123 | externalDocs?: ExternalDocumentationObject; 124 | operationId?: string; 125 | parameters?: (ParameterObject | ReferenceObject)[]; 126 | requestBody?: RequestBodyObject | ReferenceObject; 127 | responses: ResponsesObject; 128 | callbacks?: CallbacksObject; 129 | deprecated?: boolean; 130 | security?: SecurityRequirementObject[]; 131 | servers?: ServerObject[]; 132 | } 133 | export interface ExternalDocumentationObject extends ISpecificationExtension { 134 | description?: string; 135 | url: string; 136 | } 137 | 138 | /** 139 | * The location of a parameter. 140 | * Possible values are "query", "header", "path" or "cookie". 141 | * Specification: 142 | * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-locations 143 | */ 144 | export type ParameterLocation = "query" | "header" | "path" | "cookie"; 145 | 146 | /** 147 | * The style of a parameter. 148 | * Describes how the parameter value will be serialized. 149 | * (serialization is not implemented yet) 150 | * Specification: 151 | * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#style-values 152 | */ 153 | export type ParameterStyle = 154 | | "matrix" 155 | | "label" 156 | | "form" 157 | | "simple" 158 | | "spaceDelimited" 159 | | "pipeDelimited" 160 | | "deepObject"; 161 | 162 | export interface BaseParameterObject extends ISpecificationExtension { 163 | description?: string; 164 | required?: boolean; 165 | deprecated?: boolean; 166 | allowEmptyValue?: boolean; 167 | 168 | style?: ParameterStyle; // "matrix" | "label" | "form" | "simple" | "spaceDelimited" | "pipeDelimited" | "deepObject"; 169 | explode?: boolean; 170 | allowReserved?: boolean; 171 | schema?: SchemaObject | ReferenceObject; 172 | examples?: { [param: string]: ExampleObject | ReferenceObject }; 173 | example?: any; 174 | content?: ContentObject; 175 | } 176 | 177 | export interface ParameterObject extends BaseParameterObject { 178 | name: string; 179 | in: ParameterLocation; // "query" | "header" | "path" | "cookie"; 180 | } 181 | export interface RequestBodyObject extends ISpecificationExtension { 182 | description?: string; 183 | content: ContentObject; 184 | required?: boolean; 185 | } 186 | export interface ContentObject { 187 | [mediatype: string]: MediaTypeObject; 188 | } 189 | export interface MediaTypeObject extends ISpecificationExtension { 190 | schema?: SchemaObject | ReferenceObject; 191 | examples?: ExamplesObject; 192 | example?: any; 193 | encoding?: EncodingObject; 194 | } 195 | export interface EncodingObject extends ISpecificationExtension { 196 | // [property: string]: EncodingPropertyObject; 197 | [property: string]: EncodingPropertyObject | any; // Hack for allowing ISpecificationExtension 198 | } 199 | export interface EncodingPropertyObject { 200 | contentType?: string; 201 | headers?: { [key: string]: HeaderObject | ReferenceObject }; 202 | style?: string; 203 | explode?: boolean; 204 | allowReserved?: boolean; 205 | [key: string]: any; // (any) = Hack for allowing ISpecificationExtension 206 | } 207 | export interface ResponsesObject extends ISpecificationExtension { 208 | default?: ResponseObject | ReferenceObject; 209 | 210 | // [statuscode: string]: ResponseObject | ReferenceObject; 211 | [statuscode: string]: ResponseObject | ReferenceObject | any; // (any) = Hack for allowing ISpecificationExtension 212 | } 213 | export interface ResponseObject extends ISpecificationExtension { 214 | description: string; 215 | headers?: HeadersObject; 216 | content?: ContentObject; 217 | links?: LinksObject; 218 | } 219 | export interface CallbacksObject extends ISpecificationExtension { 220 | // [name: string]: CallbackObject | ReferenceObject; 221 | [name: string]: CallbackObject | ReferenceObject | any; // Hack for allowing ISpecificationExtension 222 | } 223 | export interface CallbackObject extends ISpecificationExtension { 224 | // [name: string]: PathItemObject; 225 | [name: string]: PathItemObject | any; // Hack for allowing ISpecificationExtension 226 | } 227 | export interface HeadersObject { 228 | [name: string]: HeaderObject | ReferenceObject; 229 | } 230 | export interface ExampleObject { 231 | summary?: string; 232 | description?: string; 233 | value?: any; 234 | externalValue?: string; 235 | [property: string]: any; // Hack for allowing ISpecificationExtension 236 | } 237 | export interface LinksObject { 238 | [name: string]: LinkObject | ReferenceObject; 239 | } 240 | export interface LinkObject extends ISpecificationExtension { 241 | operationRef?: string; 242 | operationId?: string; 243 | parameters?: LinkParametersObject; 244 | requestBody?: any | string; 245 | description?: string; 246 | server?: ServerObject; 247 | [property: string]: any; // Hack for allowing ISpecificationExtension 248 | } 249 | export interface LinkParametersObject { 250 | [name: string]: any | string; 251 | } 252 | export interface HeaderObject extends BaseParameterObject {} 253 | export interface TagObject extends ISpecificationExtension { 254 | name: string; 255 | description?: string; 256 | externalDocs?: ExternalDocumentationObject; 257 | [extension: string]: any; // Hack for allowing ISpecificationExtension 258 | } 259 | export interface ExamplesObject { 260 | [name: string]: ExampleObject | ReferenceObject; 261 | } 262 | 263 | export interface ReferenceObject { 264 | $ref: string; 265 | } 266 | 267 | /** 268 | * A type guard to check if the given value is a `ReferenceObject`. 269 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 270 | * 271 | * @param obj The value to check. 272 | */ 273 | export function isReferenceObject(obj: object): obj is ReferenceObject { 274 | /* eslint-disable-next-line no-prototype-builtins */ 275 | return obj.hasOwnProperty("$ref"); 276 | } 277 | 278 | export interface SchemaObject extends ISpecificationExtension { 279 | nullable?: boolean; 280 | discriminator?: DiscriminatorObject; 281 | readOnly?: boolean; 282 | writeOnly?: boolean; 283 | xml?: XmlObject; 284 | externalDocs?: ExternalDocumentationObject; 285 | example?: any; 286 | examples?: any[]; 287 | deprecated?: boolean; 288 | 289 | type?: string; 290 | allOf?: (SchemaObject | ReferenceObject)[]; 291 | oneOf?: (SchemaObject | ReferenceObject)[]; 292 | anyOf?: (SchemaObject | ReferenceObject)[]; 293 | not?: SchemaObject | ReferenceObject; 294 | items?: SchemaObject | ReferenceObject; 295 | properties?: { [propertyName: string]: SchemaObject | ReferenceObject }; 296 | additionalProperties?: SchemaObject | ReferenceObject | boolean; 297 | description?: string; 298 | format?: string; 299 | default?: any; 300 | 301 | title?: string; 302 | multipleOf?: number; 303 | maximum?: number; 304 | exclusiveMaximum?: boolean; 305 | minimum?: number; 306 | exclusiveMinimum?: boolean; 307 | maxLength?: number; 308 | minLength?: number; 309 | pattern?: string; 310 | maxItems?: number; 311 | minItems?: number; 312 | uniqueItems?: boolean; 313 | maxProperties?: number; 314 | minProperties?: number; 315 | required?: string[]; 316 | enum?: any[]; 317 | } 318 | 319 | /** 320 | * A type guard to check if the given object is a `SchemaObject`. 321 | * Useful to distinguish from `ReferenceObject` values that can be used 322 | * in most places where `SchemaObject` is allowed. 323 | * 324 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 325 | * 326 | * @param schema The value to check. 327 | */ 328 | export function isSchemaObject( 329 | schema: SchemaObject | ReferenceObject 330 | ): schema is SchemaObject { 331 | /* eslint-disable-next-line no-prototype-builtins */ 332 | return !schema.hasOwnProperty("$ref"); 333 | } 334 | 335 | export interface SchemasObject { 336 | [schema: string]: SchemaObject; 337 | } 338 | 339 | export interface DiscriminatorObject { 340 | propertyName: string; 341 | mapping?: { [key: string]: string }; 342 | } 343 | 344 | export interface XmlObject extends ISpecificationExtension { 345 | name?: string; 346 | namespace?: string; 347 | prefix?: string; 348 | attribute?: boolean; 349 | wrapped?: boolean; 350 | } 351 | export type SecuritySchemeType = "apiKey" | "http" | "oauth2" | "openIdConnect"; 352 | 353 | export interface SecuritySchemeObject extends ISpecificationExtension { 354 | type: SecuritySchemeType; 355 | description?: string; 356 | name?: string; // required only for apiKey 357 | in?: string; // required only for apiKey 358 | scheme?: string; // required only for http 359 | bearerFormat?: string; 360 | flows?: OAuthFlowsObject; // required only for oauth2 361 | openIdConnectUrl?: string; // required only for openIdConnect 362 | } 363 | export interface OAuthFlowsObject extends ISpecificationExtension { 364 | implicit?: OAuthFlowObject; 365 | password?: OAuthFlowObject; 366 | clientCredentials?: OAuthFlowObject; 367 | authorizationCode?: OAuthFlowObject; 368 | } 369 | export interface OAuthFlowObject extends ISpecificationExtension { 370 | authorizationUrl?: string; 371 | tokenUrl?: string; 372 | refreshUrl?: string; 373 | scopes: ScopesObject; 374 | } 375 | export interface ScopesObject extends ISpecificationExtension { 376 | [scope: string]: any; // Hack for allowing ISpecificationExtension 377 | } 378 | export interface SecurityRequirementObject { 379 | [name: string]: string[]; 380 | } 381 | -------------------------------------------------------------------------------- /deno/lib/utils/openapi3-ts/SpecificationExtension.ts: -------------------------------------------------------------------------------- 1 | // Suport for Specification Extensions 2 | // as described in 3 | // https://github.com/OAI/OpenAPI-Specification/blob/3.0.0-rc0/versions/3.0.md#specificationExtensions 4 | 5 | // Specification Extensions 6 | // ^x- 7 | export interface ISpecificationExtension { 8 | // Cannot constraint to "^x-" but can filter them later to access to them 9 | [extensionName: string]: any; 10 | } 11 | 12 | export class SpecificationExtension implements ISpecificationExtension { 13 | // Cannot constraint to "^x-" but can filter them later to access to them 14 | [extensionName: string]: any; 15 | 16 | static isValidExtension(extensionName: string) { 17 | return /^x\-/.test(extensionName); 18 | } 19 | 20 | getExtension(extensionName: string): any { 21 | if (!SpecificationExtension.isValidExtension(extensionName)) { 22 | throw new Error( 23 | "Invalid specification extension: '" + 24 | extensionName + 25 | "'. Extensions must start with prefix 'x-" 26 | ); 27 | } 28 | if (this[extensionName]) { 29 | return this[extensionName]; 30 | } 31 | return null; 32 | } 33 | addExtension(extensionName: string, payload: any): void { 34 | if (!SpecificationExtension.isValidExtension(extensionName)) { 35 | throw new Error( 36 | "Invalid specification extension: '" + 37 | extensionName + 38 | "'. Extensions must start with prefix 'x-" 39 | ); 40 | } 41 | this[extensionName] = payload; 42 | } 43 | listExtensions(): string[] { 44 | const res: string[] = []; 45 | for (const propName in this) { 46 | /* eslint-disable-next-line no-prototype-builtins */ 47 | if (this.hasOwnProperty(propName)) { 48 | if (SpecificationExtension.isValidExtension(propName)) { 49 | res.push(propName); 50 | } 51 | } 52 | } 53 | return res; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/mod.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "https://deno.land/std@0.74.0/http/server.ts"; 2 | import {openApi} from "../deno_lib/mod.ts"; 3 | import {schema} from "./router.ts"; 4 | 5 | const port = 5000 6 | 7 | const server = serve({ hostname: "0.0.0.0", port }); 8 | console.log(`HTTP webserver running. Access it at: http://localhost:${port}/`); 9 | 10 | for await (const request of server) { 11 | let bodyContent = "Your user-agent is:\n\n"; 12 | bodyContent += request.headers.get("user-agent") || "Unknown"; 13 | 14 | if(request.url === '/openapi'){ 15 | request.respond({ 16 | status: 200, 17 | body: JSON.stringify(openApi(schema)), 18 | headers:new Headers({ 19 | 'content-type': 'application/json' 20 | }) 21 | }); 22 | }else{ 23 | const html = await Deno.readTextFile('index.html'); 24 | request.respond({ 25 | status: 200, 26 | body: html, 27 | headers:new Headers({ 28 | 'content-type': 'text/html' 29 | }) 30 | }); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /example/model.ts: -------------------------------------------------------------------------------- 1 | import * as z from "../deno_lib/mod.ts"; 2 | 3 | export const Error = z.object({ 4 | code: z.integer(), 5 | message: z.string(), 6 | }); 7 | 8 | export const Pet = z.object({ 9 | id: z.integer("int64"), 10 | name: z.string(), 11 | tag: z.string().optional(), 12 | }); 13 | 14 | export const Pets = z.array(z.reference("Pet", Pet)); -------------------------------------------------------------------------------- /example/router.ts: -------------------------------------------------------------------------------- 1 | import * as z from "../deno_lib/mod.ts"; 2 | import {Pet, Pets, Error} from "./model.ts"; 3 | 4 | export const schema = z.endpoints([ 5 | z.endpoint({ 6 | name: "listPets", 7 | summary: "List all pets", 8 | tags: [z.literal("pets")], 9 | method: "GET", 10 | path: [z.literal("pets")], 11 | query: { 12 | limit: z.parameter(z.integer("int32").max(100)) 13 | .description("How many items to return at one time (max 100)"), 14 | }, 15 | responses: [ 16 | z.response({ 17 | status: 200, 18 | description: "A paged array of pets", 19 | headers: { 20 | "x-next": z.parameter(z.string()) 21 | .name("x-next") 22 | .description("A link to the next page of responses"), 23 | }, 24 | body: z.body({ 25 | type: "application/json", 26 | content: z.reference("Pets", Pets), 27 | }), 28 | }), 29 | z.response({ 30 | status: "default", 31 | description: "unexpected error", 32 | body: z.body({ 33 | type: "application/json", 34 | content: z.reference("Error", Error), 35 | }), 36 | }), 37 | ], 38 | }), 39 | 40 | z.endpoint({ 41 | name: "showPetById", 42 | summary: "Info for a specific pet", 43 | tags: [z.literal("pets")], 44 | method: "GET", 45 | path: [ 46 | z.literal("pets"), 47 | z.parameter(z.string().uuid()) 48 | .name("petId") 49 | .description("The id of the pet to retrieve"), 50 | ], 51 | responses: [ 52 | z.response({ 53 | status: 200, 54 | description: "Expected response to a valid request", 55 | body: z.body({ 56 | type: "application/json", 57 | content: z.reference("Pet", Pet), 58 | }), 59 | }), 60 | z.response({ 61 | status: "default", 62 | description: "unexpected error", 63 | body: z.body({ 64 | type: "application/json", 65 | content: z.reference("Error", Error), 66 | }), 67 | }), 68 | ], 69 | }), 70 | 71 | z.endpoint({ 72 | name: "createPets", 73 | summary: "Create a pet", 74 | tags: [z.literal("pets")], 75 | method: "POST", 76 | path: [z.literal("pets")], 77 | headers: { 78 | accept: z.parameter(z.literal("application/json")), 79 | }, 80 | responses: [ 81 | z.response({ 82 | status: 201, 83 | description: "Null response", 84 | }), 85 | z.response({ 86 | status: "default", 87 | description: "unexpected error", 88 | body: z.body({ 89 | type: "application/json", 90 | content: z.reference("Error", Error), 91 | }), 92 | }), 93 | ], 94 | }), 95 | ]); -------------------------------------------------------------------------------- /images/pets_swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flock-community/zod-endpoints/d7085000cabaa2e26a5d7d0ada88536ff46b167e/images/pets_swagger.png -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": ".", 3 | "transform": { 4 | "^.+\\.tsx?$": "ts-jest" 5 | }, 6 | "testRegex": "src/.*\\.test\\.ts$", 7 | "moduleFileExtensions": [ 8 | "ts", 9 | "tsx", 10 | "js", 11 | "jsx", 12 | "json", 13 | "node" 14 | ], 15 | "coverageReporters": [ 16 | "json-summary", 17 | "text", 18 | "lcov" 19 | ], 20 | "globals": { 21 | "ts-jest": { 22 | "tsconfig": "tsconfig.json" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod-endpoints", 3 | "version": "0.1.7", 4 | "description": "Typescript contract first strictly typed endpoints", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "module": "./lib/index.mjs", 8 | "exports": { 9 | "require": "./lib/index.js", 10 | "import": "./lib/index.mjs" 11 | }, 12 | "files": [ 13 | "/lib" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/flock-community/zod-endpoints" 18 | }, 19 | "author": "Flock Community ", 20 | "license": "MIT", 21 | "sideEffects": false, 22 | "bugs": { 23 | "url": "https://github.com/flock-community/zod-endpoints/issues" 24 | }, 25 | "homepage": "https://github.com/flock-community/zod-endpoints", 26 | "dependencies": { 27 | "zod": "3.5.1" 28 | }, 29 | "keywords": [ 30 | "openapi", 31 | "endpoints", 32 | "typescript", 33 | "zod" 34 | ], 35 | "scripts": { 36 | "check:format": "prettier --check \"src/**/*.ts\" \"deno/lib/**/*.ts\"", 37 | "fix:format": "prettier --write \"src/**/*.ts\" \"deno/lib/**/*.ts\"", 38 | "check:lint": "eslint --ext .ts ./src", 39 | "fix:lint": "eslint --fix --ext .ts ./src", 40 | "check": "yarn check:lint && yarn check:format", 41 | "fix": "yarn fix:lint && yarn fix:format", 42 | "clean": "rm -rf lib/* deno/lib/*", 43 | "build": "yarn run clean && npm run build:cjs && npm run build:esm && npm run build:deno", 44 | "build:deno": "node ./deno/build.mjs", 45 | "build:esm": "rollup --config rollup.config.js", 46 | "build:cjs": "tsc --p tsconfig.cjs.json", 47 | "build:types": "tsc --p tsconfig.types.json", 48 | "rollup": "rollup --config rollup.config.js", 49 | "test": "node --trace-warnings node_modules/.bin/jest --coverage && yarn run badge", 50 | "testone": "jest", 51 | "badge": "make-coverage-badge --output-path ./coverage.svg", 52 | "prepublishOnly": "npm run test && npm run build && npm run build:deno", 53 | "play": "nodemon -e ts -w . -x ts-node src/playground.ts --project tsconfig.json --trace-warnings", 54 | "depcruise": "depcruise -c .dependency-cruiser.js src", 55 | "benchmark": "ts-node src/benchmarks/index.ts" 56 | }, 57 | "devDependencies": { 58 | "@rollup/plugin-typescript": "^8.2.0", 59 | "@types/benchmark": "^2.1.0", 60 | "@types/jest": "^26.0.17", 61 | "@types/node": "^14.14.10", 62 | "@typescript-eslint/eslint-plugin": "^4.11.1", 63 | "@typescript-eslint/parser": "^4.11.1", 64 | "benchmark": "^2.1.4", 65 | "dependency-cruiser": "^9.19.0", 66 | "eslint": "^7.15.0", 67 | "eslint-config-prettier": "^7.1.0", 68 | "eslint-plugin-import": "^2.22.1", 69 | "eslint-plugin-simple-import-sort": "^7.0.0", 70 | "eslint-plugin-unused-imports": "^1.1.0", 71 | "husky": "^4.3.4", 72 | "jest": "^26.6.3", 73 | "lint-staged": "^10.5.3", 74 | "make-coverage-badge": "^1.2.0", 75 | "nodemon": "^2.0.2", 76 | "prettier": "^2.2.1", 77 | "rollup": "^2.42.1", 78 | "rollup-plugin-uglify": "^6.0.4", 79 | "ts-jest": "^26.4.4", 80 | "ts-node": "^9.1.0", 81 | "tslib": "^2.1.0", 82 | "typescript": "^4.3.2" 83 | }, 84 | "husky": { 85 | "hooks": { 86 | "pre-commit": "lint-staged && yarn build:deno && git add .", 87 | "pre-push": "lint-staged && yarn build && yarn test" 88 | } 89 | }, 90 | "lint-staged": { 91 | "*.ts": [ 92 | "yarn fix:lint", 93 | "yarn fix:format" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { uglify } from "rollup-plugin-uglify"; 4 | 5 | export default [ 6 | { 7 | input: "src/index.ts", 8 | output: [ 9 | { 10 | file: "lib/index.mjs", 11 | format: "es", 12 | sourcemap: true, 13 | }, 14 | ], 15 | plugins: [ 16 | typescript({ 17 | tsconfig: "tsconfig.esm.json", 18 | sourceMap: true, 19 | }), 20 | uglify(), 21 | ], 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/__tests__/client.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | 6 | const project = z.object({ 7 | id: z.string().uuid(), 8 | name: z.string(), 9 | }); 10 | 11 | const error = z.object({ 12 | code: z.number(), 13 | message: z.string(), 14 | }); 15 | 16 | const errorResponse = z.response({ 17 | status: 500, 18 | description: "Error occurred", 19 | body: z.body({ 20 | type: "application/json", 21 | content: error, 22 | }), 23 | }); 24 | 25 | const schema = z.endpoints([ 26 | z.endpoint({ 27 | name: "GET_PROJECT", 28 | method: "GET", 29 | path: z.path("projects", z.string().uuid()), 30 | responses: [ 31 | z.response({ 32 | description: "Found project", 33 | status: 200, 34 | body: z.body({ 35 | type: "application/json", 36 | content: project, 37 | }), 38 | }), 39 | z.response({ 40 | description: "Not found", 41 | status: 404, 42 | body: z.body({ 43 | type: "application/json", 44 | content: error, 45 | }), 46 | }), 47 | errorResponse, 48 | ], 49 | }), 50 | z.endpoint({ 51 | name: "LIST_PROJECT", 52 | method: "GET", 53 | path: z.path("projects"), 54 | headers: {}, 55 | responses: [ 56 | z.response({ 57 | description: "Found project", 58 | status: 200, 59 | body: z.body({ 60 | type: "application/json", 61 | content: z.array(project), 62 | }), 63 | }), 64 | errorResponse, 65 | ], 66 | }), 67 | z.endpoint({ 68 | name: "CREATE_PROJECT", 69 | method: "POST", 70 | path: z.path("projects"), 71 | body: z.body({ 72 | type: "application/json", 73 | content: project, 74 | }), 75 | responses: [ 76 | z.response({ 77 | description: "Created project", 78 | status: 201, 79 | }), 80 | errorResponse, 81 | ], 82 | }), 83 | ]); 84 | 85 | const id = "1a2c8758-e223-11eb-ba80-0242ac130004" as string; 86 | const state: z.infer[] = []; 87 | 88 | // @ts-ignore 89 | const client: z.Client = (req) => { 90 | console.log(req); 91 | if (req && req.body && req.method === "POST" && req.path[0] == "projects") { 92 | state.push(req.body.content); 93 | return Promise.resolve({ 94 | status: 201, 95 | body: { 96 | type: "application/json", 97 | content: state, 98 | }, 99 | headers: {}, 100 | }); 101 | } 102 | 103 | if (req.method === "GET" && req.path[1] == id) { 104 | return Promise.resolve({ 105 | status: 200, 106 | body: { 107 | type: "application/json", 108 | content: state.find((it) => it.id === id), 109 | }, 110 | headers: {}, 111 | }); 112 | } 113 | 114 | if (req.method === "GET" && req.path[0] == "projects") { 115 | return Promise.resolve({ 116 | status: 200, 117 | body: { 118 | type: "application/json", 119 | content: state, 120 | }, 121 | headers: {}, 122 | }); 123 | } 124 | 125 | throw new Error("Cannot respond"); 126 | }; 127 | 128 | test("client", async () => { 129 | const resPost = await client({ 130 | method: "POST", 131 | path: ["projects"], 132 | body: { 133 | type: "application/json", 134 | content: { 135 | id: id, 136 | name: "Todo 12", 137 | }, 138 | }, 139 | query: {}, 140 | headers: {}, 141 | }); 142 | 143 | expect(resPost.status).toBe(201); 144 | 145 | const resGetList = await client({ 146 | method: "GET", 147 | path: ["projects"], 148 | query: {}, 149 | headers: {}, 150 | }); 151 | 152 | expect(resGetList.status).toBe(200); 153 | if (resGetList.status == 200) { 154 | expect(resGetList.body.content.find((it) => it.id === id)?.id).toBe(id); 155 | } 156 | 157 | const resGetId = await client({ 158 | method: "GET", 159 | path: ["projects", id], 160 | query: {}, 161 | headers: {}, 162 | }); 163 | 164 | expect(resGetId.status).toBe(200); 165 | if (resGetId.status == 200) { 166 | expect(resGetId.body.content.id).toBe(id); 167 | } 168 | }); 169 | 170 | test("openapi", () => { 171 | const openApi = z.openApi(schema); 172 | 173 | const assert = Object.entries(openApi.paths).flatMap(([key, value]) => 174 | Object.entries(value).map(([_, req]) => [ 175 | key, 176 | // @ts-ignore 177 | req.parameters?.map((par) => par.name), 178 | ]) 179 | ); 180 | expect(JSON.stringify(assert)).toBe( 181 | JSON.stringify([ 182 | ["/projects/{param_1}", ["param_1"]], 183 | ["/projects", undefined], 184 | ["/projects", undefined], 185 | ]) 186 | ); 187 | }); 188 | -------------------------------------------------------------------------------- /src/__tests__/match.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | 6 | test("match requset", () => { 7 | const a = z.endpoint({ 8 | name: "A", 9 | method: "GET", 10 | path: [z.literal("a")], 11 | query: { 12 | next: z.parameter(z.string()), 13 | }, 14 | responses: [ 15 | z.response({ 16 | status: 200, 17 | }), 18 | ], 19 | }); 20 | 21 | const b = z.endpoint({ 22 | name: "B", 23 | method: "POST", 24 | path: [z.literal("b")], 25 | query: { 26 | next: z.parameter(z.string().optional()), 27 | }, 28 | responses: [ 29 | z.response({ 30 | status: 200, 31 | body: [ 32 | z.body({ 33 | type: "application/json", 34 | content: z.object({ 35 | b: z.string(), 36 | }), 37 | }), 38 | z.body({ 39 | type: "plain/text", 40 | content: z.object({ 41 | c: z.string(), 42 | }), 43 | }), 44 | ], 45 | }), 46 | ], 47 | }); 48 | const schema = z.union([a, b]); 49 | 50 | const reqA: z.MatchRequest = { 51 | method: "GET", 52 | path: ["a"], 53 | query: { 54 | next: "a", 55 | }, 56 | headers: {}, 57 | }; 58 | 59 | const reqB: z.MatchRequest = { 60 | method: "POST", 61 | path: ["b"], 62 | query: { 63 | next: undefined, 64 | }, 65 | headers: {}, 66 | }; 67 | 68 | expect(z.matchRequest(schema, reqA)).toEqual(a); 69 | expect(z.matchRequest(schema, reqB)).toEqual(b); 70 | }); 71 | -------------------------------------------------------------------------------- /src/__tests__/param.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | 6 | test("parameter with number", () => { 7 | const n = z 8 | .parameter(z.number().max(100)) 9 | .name("limit") 10 | .description("How many items to return at one time (max 100)"); 11 | 12 | expect(n.parse(50)).toEqual(50); 13 | 14 | try { 15 | n.parse(400); 16 | } catch (err) { 17 | const zerr: z.ZodError = err; 18 | expect(zerr.issues[0].code).toEqual(z.ZodIssueCode.too_big); 19 | expect(zerr.issues[0].message).toEqual( 20 | `Value should be less than or equal to 100` 21 | ); 22 | } 23 | }); 24 | 25 | test("parameter with string", () => { 26 | const s = z 27 | .parameter(z.string().max(7)) 28 | .name("limit") 29 | .description("How many items to return at one time (max 100)"); 30 | 31 | expect(s.parse("123456")).toEqual("123456"); 32 | 33 | try { 34 | s.parse("12345678"); 35 | } catch (err) { 36 | const zerr: z.ZodError = err; 37 | expect(zerr.issues[0].code).toEqual(z.ZodIssueCode.too_big); 38 | expect(zerr.issues[0].message).toEqual( 39 | `Should be at most 7 characters long` 40 | ); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /src/__tests__/parse.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | 6 | test("parse bigint", () => { 7 | const bigint = z.bigint(); 8 | const res = bigint.parse(BigInt(2)); 9 | expect(res).toEqual(BigInt(2)); 10 | }); 11 | 12 | test("parse pet", () => { 13 | const Pet = z.object({ 14 | id: z.bigint(), 15 | name: z.string(), 16 | tag: z.string().optional(), 17 | }); 18 | const Pets = z.array(z.reference("Pet", Pet)); 19 | 20 | const arr = [ 21 | { id: BigInt(0), name: "a", tag: "Test" }, 22 | { id: BigInt(1), name: "b", tag: "Test" }, 23 | ]; 24 | 25 | expect(Pets.parse(arr)).toEqual([ 26 | { id: BigInt(0), name: "a", tag: "Test" }, 27 | { id: BigInt(1), name: "b", tag: "Test" }, 28 | ]); 29 | }); 30 | -------------------------------------------------------------------------------- /src/__tests__/pets_endpoints.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import petApi from "../data/petstore"; 5 | import * as z from "../index"; 6 | import { openApi } from "../openapi"; 7 | 8 | const Error = z.object({ 9 | code: z.integer(), 10 | message: z.string(), 11 | }); 12 | 13 | const Pet = z.object({ 14 | id: z.integer("int64"), 15 | name: z.string(), 16 | tag: z.string().optional(), 17 | }); 18 | 19 | const Pets = z.array(z.reference("Pet", Pet)); 20 | 21 | const schema = z.endpoints([ 22 | z.endpoint({ 23 | name: "listPets", 24 | summary: "List all pets", 25 | tags: [z.literal("pets")], 26 | method: "GET", 27 | path: [z.literal("pets")], 28 | query: { 29 | limit: z 30 | .parameter(z.integer("int32").max(100).optional()) 31 | .description("How many items to return at one time (max 100)"), 32 | }, 33 | responses: [ 34 | z.response({ 35 | status: 200, 36 | description: "A paged array of pets", 37 | headers: { 38 | "x-next": z 39 | .parameter(z.string()) 40 | .name("x-next") 41 | .description("A link to the next page of responses"), 42 | }, 43 | body: z.body({ 44 | type: "application/json", 45 | content: z.reference("Pets", Pets), 46 | }), 47 | }), 48 | z.response({ 49 | status: "default", 50 | description: "unexpected error", 51 | body: z.body({ 52 | type: "application/json", 53 | content: z.reference("Error", Error), 54 | }), 55 | }), 56 | ], 57 | }), 58 | 59 | z.endpoint({ 60 | name: "showPetById" as const, 61 | summary: "Info for a specific pet", 62 | tags: [z.literal("pets")], 63 | method: "GET", 64 | path: [ 65 | z.literal("pets"), 66 | z 67 | .parameter(z.string().uuid()) 68 | .name("petId") 69 | .description("The id of the pet to retrieve"), 70 | ], 71 | responses: [ 72 | z.response({ 73 | status: 200, 74 | description: "Expected response to a valid request", 75 | body: z.body({ 76 | type: "application/json", 77 | content: z.reference("Pet", Pet), 78 | }), 79 | }), 80 | z.response({ 81 | status: "default", 82 | description: "unexpected error", 83 | body: z.body({ 84 | type: "application/json", 85 | content: z.reference("Error", Error), 86 | }), 87 | }), 88 | ], 89 | }), 90 | 91 | z.endpoint({ 92 | name: "createPets" as const, 93 | summary: "Create a pet", 94 | tags: [z.literal("pets")], 95 | method: "POST", 96 | path: [z.literal("pets")], 97 | headers: { 98 | accept: z.parameter(z.literal("application/json")), 99 | }, 100 | responses: [ 101 | z.response({ 102 | status: 201, 103 | description: "Null response", 104 | }), 105 | z.response({ 106 | status: "default", 107 | description: "unexpected error", 108 | body: z.body({ 109 | type: "application/json", 110 | content: z.reference("Error", Error), 111 | }), 112 | }), 113 | ], 114 | }), 115 | ]); 116 | 117 | const server = { url: "http://petstore.swagger.io/v1" }; 118 | const api = openApi( 119 | schema, 120 | { version: "1.0.0", title: "Swagger Petstore", license: { name: "MIT" } }, 121 | [server] 122 | ); 123 | 124 | test("compare open api schema", () => { 125 | function compare(actual: unknown, expected: unknown) { 126 | const value = JSON.parse(JSON.stringify(actual)); 127 | expect(value).toEqual(expected); 128 | } 129 | 130 | compare(api.paths["/pets"].get, petApi.paths["/pets"].get); 131 | compare(api.paths["/pets/{petId}"].get, petApi.paths["/pets/{petId}"].get); 132 | compare(api.paths["/pets"].post, petApi.paths["/pets"].post); 133 | compare(api.components?.schemas?.Error, petApi.components?.schemas?.Error); 134 | compare(api.components?.schemas?.Pet, petApi.components?.schemas?.Pet); 135 | compare(api.components?.schemas?.Pets, petApi.components?.schemas?.Pets); 136 | compare(api, petApi); 137 | }); 138 | 139 | test("validate example request", () => { 140 | type Input = z.input; 141 | 142 | const listPets: Input = { 143 | path: ["pets"], 144 | method: "GET", 145 | query: { 146 | limit: 10, 147 | }, 148 | headers: {}, 149 | responses: { 150 | description: "A paged array of pets", 151 | status: 200, 152 | headers: { 153 | "x-next": "?", 154 | }, 155 | body: { 156 | type: "application/json", 157 | content: [ 158 | { 159 | id: 1, 160 | name: "Bello", 161 | tag: "DOG", 162 | }, 163 | ], 164 | }, 165 | }, 166 | }; 167 | expect(schema.parse(listPets).name).toEqual("listPets"); 168 | 169 | const showPetById: Input = { 170 | path: ["pets", "b945f0a8-022d-11eb-adc1-0242ac120002"], 171 | method: "GET", 172 | query: {}, 173 | headers: {}, 174 | responses: { 175 | description: "unexpected error", 176 | status: "default", 177 | headers: undefined, 178 | body: { 179 | type: "application/json", 180 | content: { 181 | code: 50, 182 | message: "This is an error", 183 | }, 184 | }, 185 | }, 186 | }; 187 | const res = schema.parse(showPetById); 188 | expect(res.name).toEqual("showPetById"); 189 | }); 190 | 191 | test("api interface", () => { 192 | const api: z.Api = { 193 | listPets: () => 194 | Promise.resolve({ 195 | status: 200, 196 | headers: { "x-next": "abc" }, 197 | body: { type: "application/json", content: [] }, 198 | }), 199 | showPetById: () => 200 | Promise.resolve({ 201 | status: 200, 202 | body: { type: "application/json", content: { id: 1, name: "Pet" } }, 203 | }), 204 | createPets: () => Promise.resolve({ status: 201 }), 205 | }; 206 | 207 | expect(api).toBeTruthy(); 208 | }); 209 | -------------------------------------------------------------------------------- /src/__tests__/pets_raw.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import petApi from "../data/petstore"; 5 | import * as z from "../index"; 6 | import { openApi } from "../openapi"; 7 | 8 | const Error = z.object({ 9 | code: z.integer(), 10 | message: z.string(), 11 | }); 12 | 13 | const Pet = z.object({ 14 | id: z.integer("int64"), 15 | name: z.string(), 16 | tag: z.string().optional(), 17 | }); 18 | 19 | const Pets = z.array(z.reference("Pet", Pet)); 20 | 21 | const schema = z.union([ 22 | z.object({ 23 | name: z.literal("listPets").default("listPets"), 24 | summary: z.literal("List all pets").default("List all pets"), 25 | tags: z.tuple([z.literal("pets")]).default(["pets"]), 26 | path: z.tuple([z.literal("pets")]), 27 | method: z.literal("GET"), 28 | query: z.object({ 29 | limit: z 30 | .parameter(z.integer("int32").max(100).optional()) 31 | .description("How many items to return at one time (max 100)"), 32 | }), 33 | headers: z.object({}), 34 | body: z.undefined(), 35 | responses: z.union([ 36 | z.object({ 37 | status: z.literal(200), 38 | description: z.literal("A paged array of pets"), 39 | headers: z.object({ 40 | "x-next": z 41 | .parameter(z.string()) 42 | .name("x-next") 43 | .description("A link to the next page of responses"), 44 | }), 45 | body: z.object({ 46 | type: z.literal("application/json"), 47 | content: z.reference("Pets", Pets), 48 | }), 49 | }), 50 | z.object({ 51 | status: z.literal("default"), 52 | description: z.literal("unexpected error"), 53 | headers: z.object({}), 54 | body: z.object({ 55 | type: z.literal("application/json"), 56 | content: z.reference("Error", Error), 57 | }), 58 | }), 59 | ]), 60 | }), 61 | z.object({ 62 | name: z.literal("showPetById").default("showPetById"), 63 | summary: z 64 | .literal("Info for a specific pet") 65 | .default("Info for a specific pet"), 66 | tags: z.tuple([z.literal("pets")]).default(["pets"]), 67 | path: z.tuple([ 68 | z.literal("pets"), 69 | z 70 | .parameter(z.string().uuid()) 71 | .name("petId") 72 | .description("The id of the pet to retrieve"), 73 | ]), 74 | method: z.literal("GET"), 75 | query: z.object({}), 76 | headers: z.object({}), 77 | body: z.undefined(), 78 | responses: z.union([ 79 | z.object({ 80 | status: z.literal(200), 81 | description: z.literal("Expected response to a valid request"), 82 | headers: z.object({}), 83 | body: z.object({ 84 | type: z.literal("application/json"), 85 | content: z.reference("Pet", Pet), 86 | }), 87 | }), 88 | z.object({ 89 | status: z.literal("default"), 90 | description: z.literal("unexpected error"), 91 | headers: z.object({}), 92 | body: z.object({ 93 | type: z.literal("application/json"), 94 | content: z.reference("Error", Error), 95 | }), 96 | }), 97 | ]), 98 | }), 99 | z.object({ 100 | name: z.literal("createPets").default("createPets"), 101 | summary: z.literal("Create a pet").default("Create a pet"), 102 | tags: z.tuple([z.literal("pets")]).default(["pets"]), 103 | path: z.tuple([z.literal("pets")]), 104 | method: z.literal("POST"), 105 | query: z.object({}), 106 | headers: z.object({ 107 | accept: z.parameter(z.literal("application/json")), 108 | }), 109 | body: z.undefined(), 110 | responses: z.union([ 111 | z.object({ 112 | status: z.literal(201), 113 | description: z.literal("Null response"), 114 | headers: z.object({}), 115 | body: z.undefined(), 116 | }), 117 | z.object({ 118 | status: z.literal("default"), 119 | description: z.literal("unexpected error"), 120 | headers: z.object({}), 121 | body: z.object({ 122 | type: z.literal("application/json"), 123 | content: z.reference("Error", Error), 124 | }), 125 | }), 126 | ]), 127 | }), 128 | ]); 129 | 130 | test("api interface", () => { 131 | const api: z.Api = { 132 | listPets: () => 133 | Promise.resolve({ 134 | status: 200, 135 | headers: { "x-next": "abc" }, 136 | body: { type: "application/json", content: [] }, 137 | }), 138 | showPetById: () => 139 | Promise.resolve({ 140 | status: 200, 141 | headers: {}, 142 | body: { type: "application/json", content: { id: 1, name: "Pet" } }, 143 | }), 144 | createPets: () => Promise.resolve({ status: 201, headers: {} }), 145 | }; 146 | 147 | expect(api).toBeTruthy(); 148 | }); 149 | 150 | test("client interface", async () => { 151 | // @ts-ignore 152 | const client: z.Client = (req) => { 153 | const match = z.matchRequest(schema, req); 154 | return Promise.resolve({ 155 | status: 200, 156 | headers: { 157 | "x-next": "xxx", 158 | }, 159 | body: { 160 | type: "application/json", 161 | content: [ 162 | { 163 | id: 123, 164 | name: match?.shape.name.parse(undefined) ?? "", 165 | }, 166 | ], 167 | }, 168 | } as const); 169 | }; 170 | 171 | const res = await client({ 172 | method: "GET", 173 | path: ["pets"], 174 | headers: {}, 175 | query: {}, 176 | body: undefined, 177 | }); 178 | 179 | expect(res.body).toEqual({ 180 | type: "application/json", 181 | content: [{ id: 123, name: "listPets" }], 182 | }); 183 | }); 184 | 185 | test("compare open api schema", async () => { 186 | const server = { url: "http://petstore.swagger.io/v1" }; 187 | const api = openApi( 188 | schema, 189 | { 190 | version: "1.0.0", 191 | title: "Swagger Petstore", 192 | license: { name: "MIT" }, 193 | }, 194 | [server] 195 | ); 196 | 197 | function compare(actual: unknown, expected: unknown) { 198 | const value = JSON.parse(JSON.stringify(actual)); 199 | expect(value).toEqual(expected); 200 | } 201 | 202 | compare(api.paths["/pets"].get, petApi.paths["/pets"].get); 203 | compare(api.paths["/pets/{petId}"].get, petApi.paths["/pets/{petId}"].get); 204 | compare(api.paths["/pets"].post, petApi.paths["/pets"].post); 205 | compare(api.components?.schemas?.Error, petApi.components?.schemas?.Error); 206 | compare(api.components?.schemas?.Pet, petApi.components?.schemas?.Pet); 207 | compare(api.components?.schemas?.Pets, petApi.components?.schemas?.Pets); 208 | compare(api, petApi); 209 | }); 210 | 211 | test("validate example request", () => { 212 | type Input = z.input; 213 | 214 | const listPets: Input = { 215 | path: ["pets"], 216 | method: "GET", 217 | query: { 218 | limit: 10, 219 | }, 220 | headers: {}, 221 | 222 | responses: { 223 | status: 200, 224 | description: "A paged array of pets", 225 | headers: { 226 | "x-next": "?", 227 | }, 228 | body: { 229 | type: "application/json", 230 | content: [ 231 | { 232 | id: 1, 233 | name: "Bello", 234 | tag: "DOG", 235 | }, 236 | ], 237 | }, 238 | }, 239 | }; 240 | expect(schema.parse(listPets).name).toEqual("listPets"); 241 | 242 | const showPetById: Input = { 243 | path: ["pets", "b945f0a8-022d-11eb-adc1-0242ac120002"], 244 | method: "GET", 245 | query: {}, 246 | headers: {}, 247 | responses: { 248 | status: "default", 249 | description: "unexpected error", 250 | headers: {}, 251 | body: { 252 | type: "application/json", 253 | content: { 254 | code: 50, 255 | message: "This is an error", 256 | }, 257 | }, 258 | }, 259 | }; 260 | expect(schema.parse(showPetById).name).toEqual("showPetById"); 261 | }); 262 | -------------------------------------------------------------------------------- /src/__tests__/project.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | import { component, parameter } from "../index"; 6 | import { openApi } from "../openapi"; 7 | import { OpenAPIObject } from "../utils/openapi3-ts/OpenApi"; 8 | 9 | test("test project", () => { 10 | const route: z.Http = { 11 | name: z.literal("GET_USER").default("GET_USER"), 12 | method: z.literal("GET"), 13 | path: z.tuple([z.literal("user")]), 14 | summary: z.undefined(), 15 | tags: z.tuple([z.literal("a"), z.literal("b")]).default(["a", "b"]), 16 | query: z.object({ 17 | test: parameter(z.number().max(100).optional()).description( 18 | "How many items to return at one time (max 100)" 19 | ), 20 | }), 21 | headers: z.object({}), 22 | body: z.undefined(), 23 | responses: z.union([ 24 | z.object({ 25 | description: z.literal("List of projects"), 26 | status: z.literal(200), 27 | headers: z.object({}), 28 | body: z.object({ 29 | type: z.literal("application/json"), 30 | content: component( 31 | z.object({ 32 | uuid: z.string().uuid(), 33 | name: z.string(), 34 | }) 35 | ), 36 | }), 37 | }), 38 | z.object({ 39 | description: z.literal("Not projects found"), 40 | status: z.literal(404), 41 | headers: z.object({}), 42 | body: z.undefined(), 43 | }), 44 | ]), 45 | }; 46 | 47 | const res = openApi(z.object(route)); 48 | 49 | const exp: OpenAPIObject = { 50 | openapi: "3.0.0", 51 | info: { 52 | version: "1.0.0", 53 | title: "No title", 54 | }, 55 | servers: [], 56 | paths: { 57 | "/user": { 58 | get: { 59 | operationId: "GET_USER", 60 | parameters: [ 61 | { 62 | description: "How many items to return at one time (max 100)", 63 | in: "query", 64 | name: "test", 65 | required: false, 66 | schema: { 67 | format: "int32", 68 | type: "integer", 69 | }, 70 | }, 71 | ], 72 | summary: undefined, 73 | tags: ["a", "b"], 74 | requestBody: undefined, 75 | responses: { 76 | "200": { 77 | description: "List of projects", 78 | headers: undefined, 79 | content: { 80 | "application/json": { 81 | schema: { 82 | properties: { 83 | name: { 84 | type: "string", 85 | }, 86 | uuid: { 87 | type: "string", 88 | }, 89 | }, 90 | required: ["uuid", "name"], 91 | type: "object", 92 | }, 93 | }, 94 | }, 95 | }, 96 | "404": { 97 | description: "Not projects found", 98 | headers: undefined, 99 | content: undefined, 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | components: undefined, 106 | }; 107 | expect(res).toEqual(exp); 108 | }); 109 | -------------------------------------------------------------------------------- /src/__tests__/router_body.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | import { openApi } from "../openapi"; 6 | 7 | test("router with body", async () => { 8 | const pet = z.reference( 9 | "Pets", 10 | z.object({ 11 | id: z.integer("int64"), 12 | name: z.string(), 13 | tag: z.string().optional(), 14 | }) 15 | ); 16 | 17 | const schema = z.endpoints([ 18 | z.endpoint({ 19 | name: "C", 20 | method: "POST", 21 | path: [z.literal("pets")], 22 | body: z.body({ 23 | type: "application/json", 24 | content: pet, 25 | }), 26 | responses: [ 27 | z.response({ 28 | status: 201, 29 | headers: {}, 30 | description: "Post with body", 31 | }), 32 | ], 33 | }), 34 | ]); 35 | 36 | const api: z.Api = { 37 | C: () => 38 | Promise.resolve({ 39 | status: 201, 40 | headers: {}, 41 | }), 42 | }; 43 | 44 | const res = await api["C"]({ 45 | method: "POST", 46 | path: ["pets"], 47 | query: {}, 48 | headers: {}, 49 | body: { type: "application/json", content: { id: 1, name: "Joe" } }, 50 | }); 51 | expect(res).toEqual({ 52 | status: 201, 53 | headers: {}, 54 | }); 55 | 56 | const open = openApi(schema); 57 | expect(open).toEqual({ 58 | components: undefined, 59 | info: { 60 | title: "No title", 61 | version: "1.0.0", 62 | }, 63 | openapi: "3.0.0", 64 | paths: { 65 | "/pets": { 66 | post: { 67 | operationId: "C", 68 | parameters: undefined, 69 | requestBody: { 70 | content: { 71 | "application/json": { 72 | schema: { 73 | $ref: "#/components/schemas/Pets", 74 | }, 75 | }, 76 | }, 77 | }, 78 | responses: { 79 | 201: { 80 | content: undefined, 81 | description: "Post with body", 82 | headers: undefined, 83 | }, 84 | }, 85 | summary: undefined, 86 | tags: undefined, 87 | }, 88 | }, 89 | }, 90 | servers: [], 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/__tests__/router_minimal.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | import { openApi } from "../openapi"; 6 | 7 | test("minimal one endpoint", async () => { 8 | const schema = z.endpoints([ 9 | z.endpoint({ 10 | name: "A", 11 | method: "GET", 12 | responses: [ 13 | z.response({ 14 | status: 200, 15 | }), 16 | ], 17 | }), 18 | ]); 19 | 20 | const api: z.Api = { 21 | A: ({ path }) => { 22 | expect(path[0]).toEqual(""); 23 | return Promise.resolve({ 24 | status: 200, 25 | }); 26 | }, 27 | }; 28 | 29 | const res = await api["A"]({ 30 | method: "GET", 31 | path: [""], 32 | query: {}, 33 | headers: {}, 34 | }); 35 | expect(res).toEqual({ status: 200 }); 36 | }); 37 | 38 | test("minimal endpoint two endpoints", async () => { 39 | const schema = z.union([ 40 | z.endpoint({ 41 | name: "A", 42 | method: "GET", 43 | path: [z.literal("a")], 44 | responses: [ 45 | z.response({ 46 | status: 200, 47 | }), 48 | ], 49 | }), 50 | z.endpoint({ 51 | name: "B", 52 | method: "POST", 53 | path: [z.literal("b")], 54 | responses: [ 55 | z.response({ 56 | status: 200, 57 | body: [ 58 | z.body({ 59 | type: "application/json", 60 | content: z.object({ 61 | b: z.string(), 62 | }), 63 | }), 64 | z.body({ 65 | type: "plain/text", 66 | content: z.object({ 67 | c: z.string(), 68 | }), 69 | }), 70 | ], 71 | }), 72 | ], 73 | }), 74 | ]); 75 | 76 | const api: z.Api = { 77 | A: () => 78 | Promise.resolve({ 79 | status: 200, 80 | }), 81 | B: () => 82 | Promise.resolve({ 83 | status: 200, 84 | body: { type: "application/json", content: { b: "b" } }, 85 | }), 86 | }; 87 | 88 | const res = await api["B"]({ 89 | method: "POST", 90 | path: ["b"], 91 | query: {}, 92 | headers: {}, 93 | }); 94 | expect(res).toEqual({ 95 | status: 200, 96 | body: { 97 | type: "application/json", 98 | content: { b: "b" }, 99 | }, 100 | }); 101 | 102 | const open = openApi(schema); 103 | expect(open).toEqual({ 104 | components: undefined, 105 | info: { 106 | title: "No title", 107 | version: "1.0.0", 108 | }, 109 | openapi: "3.0.0", 110 | paths: { 111 | "/a": { 112 | get: { 113 | operationId: "A", 114 | parameters: undefined, 115 | requestBody: undefined, 116 | responses: { 117 | 200: { 118 | content: undefined, 119 | description: undefined, 120 | headers: undefined, 121 | }, 122 | }, 123 | summary: undefined, 124 | tags: undefined, 125 | }, 126 | }, 127 | "/b": { 128 | post: { 129 | operationId: "B", 130 | parameters: undefined, 131 | requestBody: undefined, 132 | responses: { 133 | 200: { 134 | content: { 135 | "application/json": { 136 | schema: { 137 | properties: { 138 | b: { 139 | type: "string", 140 | }, 141 | }, 142 | required: ["b"], 143 | type: "object", 144 | }, 145 | }, 146 | "plain/text": { 147 | schema: { 148 | properties: { 149 | c: { 150 | type: "string", 151 | }, 152 | }, 153 | required: ["c"], 154 | type: "object", 155 | }, 156 | }, 157 | }, 158 | description: undefined, 159 | headers: undefined, 160 | }, 161 | }, 162 | summary: undefined, 163 | tags: undefined, 164 | }, 165 | }, 166 | }, 167 | servers: [], 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /src/__tests__/type.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore TS6133 2 | import { expect, test } from "@jest/globals"; 3 | 4 | import * as z from "../index"; 5 | 6 | test("type string", () => { 7 | const zod = z.string(); 8 | expect(z.createSchema(zod)).toEqual({ 9 | type: "string", 10 | }); 11 | }); 12 | 13 | test("type object", () => { 14 | const zod = z.object({ 15 | id: z.number(), 16 | name: z.string(), 17 | tag: z.string().optional(), 18 | }); 19 | expect(z.createSchema(zod)).toEqual({ 20 | type: "object", 21 | required: ["id", "name"], 22 | properties: { 23 | id: { 24 | type: "integer", 25 | format: "int32", 26 | }, 27 | name: { 28 | type: "string", 29 | }, 30 | tag: { 31 | type: "string", 32 | }, 33 | }, 34 | }); 35 | }); 36 | 37 | test("type nested object", () => { 38 | const zod = z.object({ 39 | id: z.number(), 40 | name: z.string(), 41 | obj: z 42 | .object({ 43 | test: z.string(), 44 | }) 45 | .optional(), 46 | }); 47 | expect(z.createSchema(zod)).toEqual({ 48 | type: "object", 49 | required: ["id", "name"], 50 | properties: { 51 | id: { 52 | type: "integer", 53 | format: "int32", 54 | }, 55 | name: { 56 | type: "string", 57 | }, 58 | obj: { 59 | properties: { 60 | test: { 61 | type: "string", 62 | }, 63 | }, 64 | required: ["test"], 65 | type: "object", 66 | }, 67 | }, 68 | }); 69 | }); 70 | 71 | test("type array", () => { 72 | const zod = z.array(z.string()); 73 | expect(z.createSchema(zod)).toEqual({ 74 | type: "array", 75 | items: { 76 | type: "string", 77 | }, 78 | }); 79 | }); 80 | 81 | test("type integer", () => { 82 | const int32 = z.integer(); 83 | expect(z.createSchema(int32)).toEqual({ 84 | format: "int32", 85 | type: "integer", 86 | }); 87 | 88 | const int64 = z.integer("int64").max(100); 89 | expect(z.createSchema(int64)).toEqual({ 90 | format: "int64", 91 | type: "integer", 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | import { HttpSchema } from "./model"; 3 | import { PickUnion } from "./utils"; 4 | 5 | export type ApiNames = z.output extends { 6 | name: string; 7 | } 8 | ? z.output["name"] 9 | : never; 10 | 11 | export type ApiRequestAttributes = 12 | | "method" 13 | | "path" 14 | | "query" 15 | | "headers" 16 | | "body"; 17 | export type ApiResponseAttributes = "status" | "headers" | "body"; 18 | export type ApiRequest = Extract< 19 | z.output, 20 | { name: Key } 21 | >; 22 | export type ApiResponse = ApiRequest< 23 | T, 24 | Key 25 | >["responses"]; 26 | export type ApiFunction = ( 27 | request: Readonly, ApiRequestAttributes>> 28 | ) => Readonly< 29 | Promise< 30 | ApiResponseAttributes extends keyof ApiResponse 31 | ? PickUnion, ApiResponseAttributes> 32 | : undefined 33 | > 34 | >; 35 | 36 | export type Api = { 37 | [Key in ApiNames]: ApiFunction; 38 | }; 39 | 40 | export type ApiFragment> = Pick< 41 | Api, 42 | Key 43 | >; 44 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | import { HttpSchema } from "./model"; 3 | import { PickUnion } from "./utils"; 4 | 5 | type ClientRequestAttributes = "method" | "path" | "query" | "headers" | "body"; 6 | type ClientResponseAttributes = "status" | "headers" | "body"; 7 | type ClientRequest = PickUnion< 8 | z.output, 9 | ClientRequestAttributes 10 | >; 11 | type ClientRequestResponses = PickUnion< 12 | z.output, 13 | ClientRequestAttributes | "responses" 14 | >; 15 | type ClientMapper< 16 | T extends HttpSchema, 17 | R extends ClientRequest 18 | > = NonNullable<{ 19 | method: R["method"]; 20 | path: R["path"]; 21 | query: R["query"]; 22 | headers: R["headers"]; 23 | body?: R["body"]; 24 | }>; 25 | type ClientMatch> = Extract< 26 | ClientRequestResponses, 27 | ClientMapper 28 | >["responses"]; 29 | 30 | export type Client = >( 31 | req: R 32 | ) => Promise< 33 | ClientResponseAttributes extends keyof ClientMatch 34 | ? PickUnion, ClientResponseAttributes> 35 | : undefined 36 | >; 37 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | 3 | export class Component extends z.ZodType< 4 | T["_output"], 5 | T["_def"], 6 | T["_input"] 7 | > { 8 | _parse( 9 | _ctx: z.ParseContext, 10 | _data: any, 11 | _parsedType: z.ZodParsedType 12 | ): z.ParseReturnType { 13 | return this.component._parse(_ctx, _data, _parsedType); 14 | } 15 | readonly component: z.ZodTypeAny; 16 | 17 | constructor(type: z.ZodTypeAny) { 18 | super(type._def); 19 | this.component = type; 20 | } 21 | 22 | public toJSON = () => this._def; 23 | 24 | static create(type: z.ZodTypeAny) { 25 | return new Component(type); 26 | } 27 | } 28 | 29 | export const component = Component.create; 30 | -------------------------------------------------------------------------------- /src/data/petstore.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | openapi: "3.0.0", 3 | info: { 4 | version: "1.0.0", 5 | title: "Swagger Petstore", 6 | license: { 7 | name: "MIT", 8 | }, 9 | }, 10 | servers: [ 11 | { 12 | url: "http://petstore.swagger.io/v1", 13 | }, 14 | ], 15 | paths: { 16 | "/pets": { 17 | get: { 18 | summary: "List all pets", 19 | operationId: "listPets", 20 | tags: ["pets"], 21 | parameters: [ 22 | { 23 | name: "limit", 24 | in: "query", 25 | description: "How many items to return at one time (max 100)", 26 | required: false, 27 | schema: { 28 | type: "integer", 29 | format: "int32", 30 | }, 31 | }, 32 | ], 33 | responses: { 34 | "200": { 35 | description: "A paged array of pets", 36 | headers: { 37 | "x-next": { 38 | description: "A link to the next page of responses", 39 | schema: { 40 | type: "string", 41 | }, 42 | }, 43 | }, 44 | content: { 45 | "application/json": { 46 | schema: { 47 | $ref: "#/components/schemas/Pets", 48 | }, 49 | }, 50 | }, 51 | }, 52 | default: { 53 | description: "unexpected error", 54 | content: { 55 | "application/json": { 56 | schema: { 57 | $ref: "#/components/schemas/Error", 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | post: { 65 | summary: "Create a pet", 66 | operationId: "createPets", 67 | tags: ["pets"], 68 | responses: { 69 | "201": { 70 | description: "Null response", 71 | }, 72 | default: { 73 | description: "unexpected error", 74 | content: { 75 | "application/json": { 76 | schema: { 77 | $ref: "#/components/schemas/Error", 78 | }, 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | "/pets/{petId}": { 86 | get: { 87 | summary: "Info for a specific pet", 88 | operationId: "showPetById", 89 | tags: ["pets"], 90 | parameters: [ 91 | { 92 | name: "petId", 93 | in: "path", 94 | required: true, 95 | description: "The id of the pet to retrieve", 96 | schema: { 97 | type: "string", 98 | }, 99 | }, 100 | ], 101 | responses: { 102 | "200": { 103 | description: "Expected response to a valid request", 104 | content: { 105 | "application/json": { 106 | schema: { 107 | $ref: "#/components/schemas/Pet", 108 | }, 109 | }, 110 | }, 111 | }, 112 | default: { 113 | description: "unexpected error", 114 | content: { 115 | "application/json": { 116 | schema: { 117 | $ref: "#/components/schemas/Error", 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | }, 126 | components: { 127 | schemas: { 128 | Pet: { 129 | type: "object", 130 | required: ["id", "name"], 131 | properties: { 132 | id: { 133 | type: "integer", 134 | format: "int64", 135 | }, 136 | name: { 137 | type: "string", 138 | }, 139 | tag: { 140 | type: "string", 141 | }, 142 | }, 143 | }, 144 | Pets: { 145 | type: "array", 146 | items: { 147 | $ref: "#/components/schemas/Pet", 148 | }, 149 | }, 150 | Error: { 151 | type: "object", 152 | required: ["code", "message"], 153 | properties: { 154 | code: { 155 | type: "integer", 156 | format: "int32", 157 | }, 158 | message: { 159 | type: "string", 160 | }, 161 | }, 162 | }, 163 | }, 164 | }, 165 | } as const; 166 | -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | export * from "zod"; 2 | -------------------------------------------------------------------------------- /src/dsl.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | import { 3 | Content, 4 | HttpBodyObject, 5 | HttpBodyUnion, 6 | HttpObject, 7 | HttpResponseObject, 8 | Path, 9 | } from "./model"; 10 | import { Parameter } from "./parameter"; 11 | 12 | export type Body = { 13 | readonly type: string; 14 | readonly content: Content; 15 | }; 16 | 17 | export type Request = { 18 | readonly name: string; 19 | readonly summary?: string; 20 | readonly tags?: [z.ZodLiteral, ...z.ZodLiteral[]] | []; 21 | readonly method: "GET" | "POST" | "PUT" | "DELETE"; 22 | readonly path?: [Path, ...Path[]]; 23 | readonly query?: { [key: string]: Parameter }; 24 | readonly headers?: { [key: string]: Parameter }; 25 | readonly body?: 26 | | [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 27 | | [HttpBodyObject] 28 | | HttpBodyObject; 29 | }; 30 | 31 | export type Response = { 32 | readonly description?: string; 33 | readonly status: number | string; 34 | readonly headers?: { [key: string]: Parameter }; 35 | readonly body?: 36 | | [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 37 | | [HttpBodyObject] 38 | | HttpBodyObject; 39 | }; 40 | 41 | export type PathInput = [string | Path, ...(string | Path)[]]; 42 | export type PathMapper = { 43 | [Index in keyof Tuple]: Tuple[Index] extends string 44 | ? z.ZodLiteral 45 | : Tuple[Index]; 46 | }; 47 | export function path(...input: T): PathMapper { 48 | // @ts-ignore 49 | return input.map((it) => (typeof it === "string" ? z.literal(it) : it)); 50 | } 51 | 52 | export type Endpoint = Request & { 53 | responses?: 54 | | [HttpResponseObject, HttpResponseObject, ...HttpResponseObject[]] 55 | | [HttpResponseObject]; 56 | }; 57 | 58 | export function endpoints(types: [T]): T; 59 | export function endpoints< 60 | T1 extends HttpObject, 61 | T2 extends HttpObject, 62 | T3 extends HttpObject 63 | >(types: [T1, T2, ...T3[]]): z.ZodUnion<[T1, T2, ...T3[]]>; 64 | export function endpoints( 65 | types: [T] | [T, T, ...T[]] 66 | ): T | z.ZodUnion<[T, T, ...T[]]> { 67 | // @ts-ignore 68 | return types.length === 1 ? types[0] : z.union<[T, T, ...T[]]>(types); 69 | } 70 | 71 | export type EndpointMapper = z.ZodObject<{ 72 | name: z.ZodDefault>; 73 | summary: T["summary"] extends string 74 | ? z.ZodDefault> 75 | : z.ZodUndefined; 76 | tags: T["tags"] extends [z.ZodTypeAny, ...z.ZodTypeAny[]] | [] 77 | ? z.ZodDefault> 78 | : z.ZodUndefined; 79 | method: z.ZodLiteral; 80 | path: T["path"] extends [Path, ...Path[]] 81 | ? z.ZodTuple 82 | : z.ZodTuple<[z.ZodLiteral<"">]>; 83 | query: T["query"] extends z.ZodRawShape 84 | ? z.ZodObject 85 | : z.ZodObject<{}>; 86 | headers: T["headers"] extends z.ZodRawShape 87 | ? z.ZodObject 88 | : z.ZodObject<{}>; 89 | body: T["body"] extends [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 90 | ? z.ZodUnion 91 | : T["body"] extends [HttpBodyObject] 92 | ? T["body"][0] 93 | : T["body"] extends HttpBodyObject 94 | ? T["body"] 95 | : z.ZodUndefined; 96 | responses: T["responses"] extends [ 97 | HttpResponseObject, 98 | HttpResponseObject, 99 | ...HttpResponseObject[] 100 | ] 101 | ? z.ZodUnion 102 | : T["responses"] extends [HttpResponseObject] 103 | ? T["responses"][0] 104 | : z.ZodUndefined; 105 | }>; 106 | 107 | export function endpoint( 108 | endpoint: Readonly 109 | ): EndpointMapper { 110 | // @ts-ignore 111 | return z.object({ 112 | // @ts-ignore 113 | name: z.literal(endpoint.name).default(endpoint.name), 114 | summary: 115 | endpoint.summary !== undefined 116 | ? // @ts-ignore 117 | z.literal(endpoint.summary).default(endpoint.summary) 118 | : z.undefined(), 119 | tags: 120 | endpoint.tags !== undefined 121 | ? // @ts-ignore 122 | z.tuple(endpoint.tags).default(endpoint.tags.map((_) => _._def.value)) 123 | : z.undefined(), 124 | method: z.literal(endpoint.method), 125 | // @ts-ignore 126 | path: endpoint.path !== undefined ? z.tuple(endpoint.path) : [], 127 | query: 128 | endpoint.query !== undefined 129 | ? z.object(endpoint.query as z.ZodRawShape) 130 | : z.object({}), 131 | headers: 132 | endpoint.headers !== undefined 133 | ? z.object(endpoint.headers as z.ZodRawShape) 134 | : z.object({}), 135 | // @ts-ignore 136 | body: transformBody(endpoint.body), 137 | // @ts-ignore 138 | responses: 139 | endpoint.responses !== undefined 140 | ? // @ts-ignore 141 | z.union(endpoint.responses) 142 | : z.undefined(), 143 | }); 144 | } 145 | 146 | export type BodyMapper = z.ZodObject<{ 147 | type: z.ZodLiteral; 148 | content: T["content"]; 149 | }>; 150 | 151 | export function body(body: Readonly): BodyMapper { 152 | return z.object({ 153 | type: z.literal(body.type), 154 | content: body.content, 155 | }); 156 | } 157 | 158 | export type ResponseMapper = z.ZodObject<{ 159 | description: T["description"] extends string 160 | ? z.ZodLiteral 161 | : z.ZodUndefined; 162 | status: z.ZodLiteral; 163 | headers: T["headers"] extends z.ZodRawShape 164 | ? z.ZodObject 165 | : z.ZodUndefined; 166 | body: T["body"] extends [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 167 | ? z.ZodUnion 168 | : T["body"] extends [HttpBodyObject] 169 | ? T["body"][0] 170 | : T["body"] extends HttpBodyObject 171 | ? T["body"] 172 | : z.ZodUndefined; 173 | }>; 174 | 175 | export function response( 176 | response: Readonly 177 | ): ResponseMapper { 178 | // @ts-ignore 179 | return z.object({ 180 | status: z.literal(response.status), 181 | description: z.literal(response.description), 182 | headers: 183 | response.headers !== undefined 184 | ? z.object(response.headers as z.ZodRawShape) 185 | : z.undefined(), 186 | body: transformBody(response.body), 187 | }); 188 | } 189 | 190 | function transformBody( 191 | body?: 192 | | [HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]] 193 | | [HttpBodyObject] 194 | | HttpBodyObject 195 | ): HttpBodyUnion { 196 | if (body === undefined) { 197 | return z.undefined(); 198 | } 199 | if (Array.isArray(body)) { 200 | if (body.length === 1) { 201 | return body[0]; 202 | } 203 | // @ts-ignore 204 | return z.union(body); 205 | } 206 | return body; 207 | } 208 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./client"; 3 | export { Component, component } from "./component"; 4 | export * from "./deps"; 5 | export * from "./dsl"; 6 | export { Integer, integer } from "./integer"; 7 | export * from "./match"; 8 | export * from "./model"; 9 | export { createSchema, openApi } from "./openapi"; 10 | export { Parameter, parameter } from "./parameter"; 11 | export { Reference, reference } from "./reference"; 12 | -------------------------------------------------------------------------------- /src/integer.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | 3 | export interface IntegerDef extends z.ZodNumberDef { 4 | format: string; 5 | } 6 | 7 | export class Integer extends z.ZodNumber { 8 | public toJSON = () => this._def; 9 | 10 | constructor(def: IntegerDef) { 11 | super(def); 12 | } 13 | static create = (format?: string): Integer => { 14 | return new Integer({ 15 | checks: [], 16 | typeName: z.ZodFirstPartyTypeKind.ZodNumber, 17 | format: format ?? "int32", 18 | }); 19 | }; 20 | } 21 | 22 | export const integer = Integer.create; 23 | -------------------------------------------------------------------------------- /src/match.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | import { 3 | HttpObject, 4 | HttpRequestObject, 5 | HttpResponseObject, 6 | HttpResponseUnion, 7 | HttpUnion, 8 | } from "./model"; 9 | 10 | export type MatchRequest = Pick< 11 | z.output, 12 | "method" | "path" | "query" | "headers" | "body" 13 | >; 14 | export type MatchResponse = Pick< 15 | z.output, 16 | "status" | "headers" | "body" 17 | >; 18 | 19 | const requestPicker = { 20 | method: true, 21 | path: true, 22 | query: true, 23 | headers: true, 24 | body: true, 25 | } as const; 26 | const responsePicker = { status: true, headers: true, body: true } as const; 27 | 28 | export function matchRequest( 29 | schema: HttpUnion, 30 | req: MatchRequest 31 | ): HttpObject | undefined { 32 | function check(request: HttpRequestObject) { 33 | try { 34 | request.pick(requestPicker).parse(req); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | return schema.options.find(check); 42 | } 43 | 44 | export function matchResponse( 45 | responses: HttpResponseUnion, 46 | res: MatchResponse 47 | ): HttpResponseObject | undefined { 48 | function check(response: HttpResponseObject) { 49 | try { 50 | response.pick(responsePicker).parse(res); 51 | return true; 52 | } catch (e) { 53 | return false; 54 | } 55 | } 56 | if ("shape" in responses) { 57 | return responses; 58 | } 59 | if ("options" in responses) { 60 | return responses.options.find(check); 61 | } 62 | return undefined; 63 | } 64 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import * as z from "./deps"; 3 | import { Parameter } from "./parameter"; 4 | import { Reference } from "./reference"; 5 | 6 | export type Path = 7 | | z.ZodLiteral 8 | | z.ZodString 9 | | z.ZodNumber 10 | | z.ZodBoolean 11 | | Parameter; 12 | export type ParameterObject = z.ZodObject<{ [key: string]: Parameter }>; 13 | export type Content = 14 | | Reference 15 | | Component 16 | | z.ZodFirstPartySchemaTypes; 17 | 18 | export type HttpBody = { 19 | type: z.ZodLiteral; 20 | content: Content; 21 | }; 22 | export type HttpBodyObject = z.ZodObject; 23 | export type HttpBodyUnion = 24 | | HttpBodyObject 25 | | z.ZodUnion<[HttpBodyObject, HttpBodyObject, ...HttpBodyObject[]]> 26 | | z.ZodUndefined; 27 | 28 | export type HttpRequest = { 29 | name: z.ZodDefault> | z.ZodUndefined; 30 | method: z.ZodLiteral; 31 | path: z.ZodTuple<[Path, ...Path[]]> | z.ZodUndefined; 32 | summary: z.ZodDefault> | z.ZodUndefined; 33 | tags: z.ZodDefault> | z.ZodUndefined; 34 | query: ParameterObject; 35 | headers: ParameterObject; 36 | body: HttpBodyUnion; 37 | }; 38 | 39 | export type HttpRequestObject = z.ZodObject; 40 | 41 | export type HttpResponse = { 42 | status: z.ZodLiteral; 43 | description: z.ZodLiteral | z.ZodUndefined; 44 | headers: ParameterObject | z.ZodUndefined; 45 | body: HttpBodyUnion; 46 | }; 47 | 48 | export type HttpResponseObject = z.ZodObject; 49 | 50 | export type HttpResponseUnion = 51 | | HttpResponseObject 52 | | z.ZodUnion< 53 | [HttpResponseObject, HttpResponseObject, ...HttpResponseObject[]] 54 | > 55 | | z.ZodUndefined; 56 | 57 | export type Http = HttpRequest & { 58 | responses: HttpResponseUnion; 59 | }; 60 | 61 | export type HttpObject = z.ZodObject; 62 | export type HttpOptions = [HttpObject, HttpObject, ...HttpObject[]]; 63 | export type HttpUnion = z.ZodUnion; 64 | export type HttpSchema = HttpObject | HttpUnion; 65 | -------------------------------------------------------------------------------- /src/openapi.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import { ZodFirstPartyTypeKind, ZodTypeAny } from "./deps"; 3 | import { 4 | HttpBodyUnion, 5 | HttpObject, 6 | HttpResponseObject, 7 | HttpResponseUnion, 8 | HttpSchema, 9 | ParameterObject as ParameterObj, 10 | Path, 11 | } from "./model"; 12 | import { Parameter } from "./parameter"; 13 | import { Reference } from "./reference"; 14 | import { 15 | ComponentsObject, 16 | ContentObject, 17 | HeadersObject, 18 | InfoObject, 19 | OpenAPIObject, 20 | OperationObject, 21 | ParameterObject, 22 | PathsObject, 23 | ReferenceObject, 24 | RequestBodyObject, 25 | ResponseObject, 26 | ResponsesObject, 27 | SchemaObject, 28 | ServerObject, 29 | } from "./utils/openapi3-ts/OpenApi"; 30 | 31 | const base = { 32 | openapi: "3.0.0", 33 | }; 34 | 35 | function mapSchema(type: ZodTypeAny): SchemaObject | undefined { 36 | switch (type._def.typeName) { 37 | case ZodFirstPartyTypeKind.ZodNumber: 38 | if ("format" in type._def) { 39 | return { 40 | type: "integer", 41 | format: type._def.format, 42 | }; 43 | } 44 | return { 45 | type: "integer", 46 | format: "int32", 47 | }; 48 | case ZodFirstPartyTypeKind.ZodBigInt: 49 | return { 50 | type: "integer", 51 | format: "int32", 52 | }; 53 | case ZodFirstPartyTypeKind.ZodString: 54 | return { 55 | type: "string", 56 | }; 57 | default: 58 | return undefined; 59 | } 60 | } 61 | 62 | export function createSchema( 63 | obj: ZodTypeAny | Reference | Component 64 | ): SchemaObject | ReferenceObject | undefined { 65 | if ("reference" in obj) { 66 | return { $ref: `#/components/schemas/${obj.state.name}` }; 67 | } 68 | if ("component" in obj) { 69 | return createSchema(obj.component); 70 | } 71 | if ("innerType" in obj._def) { 72 | return createSchema(obj._def.innerType); 73 | } 74 | if ("type" in obj._def) { 75 | return { 76 | type: "array", 77 | items: createSchema(obj._def.type), 78 | }; 79 | } 80 | if ("shape" in obj._def) { 81 | const shape = obj._def.shape(); 82 | return { 83 | type: "object", 84 | properties: Object.keys(shape).reduce((acc, cur) => { 85 | return { 86 | ...acc, 87 | [cur]: createSchema(shape[cur]), 88 | }; 89 | }, {}), 90 | required: Object.keys(shape).reduce((acc, cur) => { 91 | if (shape[cur]._def.typeName !== ZodFirstPartyTypeKind.ZodOptional) { 92 | return [...acc, cur]; 93 | } 94 | return acc; 95 | }, []), 96 | }; 97 | } 98 | if ("checks" in obj._def) { 99 | return mapSchema(obj); 100 | } 101 | return undefined; 102 | } 103 | 104 | export function openApi( 105 | schema: HttpSchema, 106 | info: InfoObject = { title: "No title", version: "1.0.0" }, 107 | servers: ServerObject[] = [] 108 | ): OpenAPIObject { 109 | const options = "options" in schema ? schema.options : [schema]; 110 | const paths = createPaths(options); 111 | const components = createComponents(options); 112 | return { ...base, info, servers, paths, components }; 113 | } 114 | 115 | function createPaths(options: HttpObject[]): PathsObject { 116 | return options.reduce((acc, cur) => { 117 | const shape = cur._def.shape(); 118 | const method = shape.method._def.value; 119 | const path = 120 | shape.path._def !== undefined && "items" in shape.path._def 121 | ? "/" + 122 | shape.path._def.items 123 | .map((p, index) => { 124 | if ("state" in p) { 125 | return `{${p.state.name}}`; 126 | } 127 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodLiteral) { 128 | return p._def.value; 129 | } 130 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodString) { 131 | return `{param_${index}}`; 132 | } 133 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodNumber) { 134 | return `{param_${index}}`; 135 | } 136 | if (p._def.typeName === ZodFirstPartyTypeKind.ZodBoolean) { 137 | return `{param_${index}}`; 138 | } 139 | }) 140 | .join("/") 141 | : "/"; 142 | return { 143 | ...acc, 144 | [path]: { 145 | ...acc[path], 146 | [method.toLowerCase()]: createOperationObject(cur), 147 | }, 148 | }; 149 | }, {}); 150 | } 151 | 152 | function createComponents(options: HttpObject[]): ComponentsObject | undefined { 153 | const schemas = options 154 | .flatMap((http) => { 155 | return mapResponsesObject(http.shape.responses); 156 | }) 157 | .map((response) => { 158 | const body = response.shape.body; 159 | if ("shape" in body) { 160 | return body.shape.content; 161 | } 162 | if ("options" in body) { 163 | body.options.map((it) => it.shape.content); 164 | } 165 | return undefined; 166 | }) 167 | .reduce((acc, cur) => { 168 | if ( 169 | cur != null && 170 | "reference" in cur && 171 | "state" in cur && 172 | cur.state.name 173 | ) { 174 | return { ...acc, [cur.state.name]: createSchema(cur.reference) }; 175 | } else { 176 | return acc; 177 | } 178 | }, {}); 179 | return Object.keys(schemas).length > 0 180 | ? { 181 | schemas: schemas, 182 | } 183 | : undefined; 184 | } 185 | 186 | function createOperationObject(http: HttpObject): OperationObject { 187 | const shape = http._def.shape(); 188 | return { 189 | summary: 190 | "defaultValue" in shape.summary._def 191 | ? shape.summary._def.defaultValue() 192 | : undefined, 193 | operationId: 194 | "defaultValue" in shape.name._def 195 | ? shape.name._def.defaultValue() 196 | : undefined, 197 | tags: 198 | "defaultValue" in shape.tags._def 199 | ? (shape.tags._def.defaultValue() as string[]) 200 | : undefined, 201 | requestBody: createRequestBody(http), 202 | parameters: createParameterObject(http), 203 | responses: createResponsesObject(shape.responses), 204 | }; 205 | } 206 | 207 | function createRequestBody( 208 | http: HttpObject 209 | ): RequestBodyObject | ReferenceObject | undefined { 210 | const shape = http._def.shape(); 211 | const content = createContentObject(shape.body); 212 | return content ? { content } : undefined; 213 | } 214 | 215 | function createParameterObject(http: HttpObject) { 216 | const shape = http._def.shape(); 217 | const res = [ 218 | ...("shape" in shape.query._def 219 | ? createQueryParameterObject(shape.query._def.shape()) 220 | : []), 221 | ...(shape.path._def && "items" in shape.path._def 222 | ? shape.path._def.items 223 | .map(createPathParameterObject) 224 | .filter((it) => it.schema !== undefined) 225 | : []), 226 | ]; 227 | return res.length > 0 ? res : undefined; 228 | } 229 | 230 | function createPathParameterObject(it: Path, index: number): ParameterObject { 231 | return { 232 | name: "state" in it && it.state.name ? it.state.name : `param_${index}`, 233 | in: "path", 234 | description: "state" in it ? it.state.description : undefined, 235 | required: !("innerType" in it._def), 236 | schema: mapSchema(it), 237 | }; 238 | } 239 | 240 | function createQueryParameterObject( 241 | it: Record 242 | ): ParameterObject[] { 243 | return Object.keys(it).map((key) => ({ 244 | name: key, 245 | in: "query", 246 | description: "state" in it[key] ? it[key].state.description : undefined, 247 | required: !("innerType" in it[key]._def), 248 | // @ts-ignore 249 | schema: 250 | "innerType" in it[key]._def 251 | ? // @ts-ignore 252 | mapSchema(it[key]._def.innerType) 253 | : mapSchema(it[key]), 254 | })); 255 | } 256 | 257 | function createResponsesObject(responses: HttpResponseUnion): ResponsesObject { 258 | if ("shape" in responses) { 259 | return createResponseObject(responses); 260 | } 261 | if ("options" in responses) { 262 | return responses.options.reduce((acc, cur) => { 263 | return { 264 | ...acc, 265 | ...createResponseObject(cur), 266 | }; 267 | }, {}); 268 | } 269 | return {}; 270 | } 271 | 272 | function mapResponsesObject( 273 | responses: HttpResponseUnion 274 | ): HttpResponseObject[] { 275 | if ("options" in responses) { 276 | return responses.options.map((it) => it); 277 | } 278 | if ("shape" in responses) { 279 | return [responses]; 280 | } 281 | return []; 282 | } 283 | 284 | function createResponseObject( 285 | response: HttpResponseObject 286 | ): Record { 287 | const shape = response._def.shape(); 288 | const name = shape.status._def.value as string; 289 | return { 290 | [name]: { 291 | description: 292 | "value" in shape.description._def ? shape.description._def.value : "", 293 | headers: 294 | "shape" in shape.headers 295 | ? createHeadersObject(shape.headers) 296 | : undefined, 297 | content: createContentObject(shape.body), 298 | }, 299 | }; 300 | } 301 | function createContentObject(body: HttpBodyUnion): ContentObject | undefined { 302 | if ("shape" in body) { 303 | return { 304 | [body.shape.type._def.value]: { 305 | schema: createSchema(body.shape.content), 306 | }, 307 | }; 308 | } 309 | if ("options" in body) { 310 | return body.options.reduce( 311 | (acc, cur) => ({ ...acc, ...createContentObject(cur) }), 312 | {} 313 | ); 314 | } 315 | return undefined; 316 | } 317 | 318 | function createHeadersObject(headers: ParameterObj): HeadersObject | undefined { 319 | const shape = headers._def.shape(); 320 | 321 | if (Object.keys(shape).length === 0) { 322 | return undefined; 323 | } 324 | 325 | return Object.keys(shape).reduce((acc, cur) => { 326 | const obj = shape[cur]; 327 | if ("value" in obj._def) { 328 | return { 329 | ...acc, 330 | [cur]: { 331 | schema: mapSchema(obj), 332 | }, 333 | }; 334 | } 335 | if ("state" in obj) { 336 | return { 337 | ...acc, 338 | [cur]: { 339 | description: obj.state.description, 340 | schema: mapSchema(obj), 341 | }, 342 | }; 343 | } 344 | return { 345 | ...acc, 346 | [cur]: {}, 347 | }; 348 | }, {}); 349 | } 350 | -------------------------------------------------------------------------------- /src/parameter.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | 3 | type ParameterState = { 4 | name?: string; 5 | description?: string; 6 | }; 7 | 8 | export class Parameter extends z.ZodType { 9 | readonly type: z.ZodType; 10 | state: ParameterState; 11 | 12 | _parse( 13 | _ctx: z.ParseContext, 14 | _data: any, 15 | _parsedType: z.ZodParsedType 16 | ): z.ParseReturnType { 17 | return this.type._parse(_ctx, _data, _parsedType); 18 | } 19 | constructor(type: z.ZodType) { 20 | super(type._def); 21 | this.type = type; 22 | this.state = { 23 | name: undefined, 24 | description: undefined, 25 | }; 26 | } 27 | 28 | public toJSON = () => this._def; 29 | 30 | public name(name: string) { 31 | this.state = { ...this.state, name }; 32 | return this; 33 | } 34 | 35 | public description(description: string) { 36 | this.state = { ...this.state, description }; 37 | return this; 38 | } 39 | 40 | static create(type: z.ZodType) { 41 | return new Parameter(type); 42 | } 43 | } 44 | 45 | export const parameter = Parameter.create; 46 | -------------------------------------------------------------------------------- /src/playground.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const x = z.literal(""); 4 | 5 | console.log(x); 6 | -------------------------------------------------------------------------------- /src/reference.ts: -------------------------------------------------------------------------------- 1 | import * as z from "./deps"; 2 | 3 | export class Reference extends z.ZodType< 4 | T["_output"], 5 | T["_def"], 6 | T["_input"] 7 | > { 8 | readonly reference: z.ZodTypeAny; 9 | state: { 10 | name?: string; 11 | }; 12 | _parse( 13 | _ctx: z.ParseContext, 14 | _data: any, 15 | _parsedType: z.ZodParsedType 16 | ): z.ParseReturnType { 17 | return this.reference._parse(_ctx, _data, _parsedType); 18 | } 19 | 20 | constructor(name: string, type: T) { 21 | super(type._def); 22 | this.reference = type; 23 | this.state = { name }; 24 | } 25 | 26 | public toJSON = () => this._def; 27 | 28 | static create(name: string, type: T) { 29 | return new Reference(name, type); 30 | } 31 | } 32 | 33 | export const reference = Reference.create; 34 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export type PickUnion = T extends any 2 | ? { [P in K]: T[P] } 3 | : never; 4 | -------------------------------------------------------------------------------- /src/utils/openapi3-ts/OpenApi.ts: -------------------------------------------------------------------------------- 1 | // Typed interfaces for OpenAPI 3.0.0-RC 2 | // see https://github.com/OAI/OpenAPI-Specification/blob/3.0.0-rc0/versions/3.0.md 3 | 4 | import { 5 | ISpecificationExtension, 6 | SpecificationExtension, 7 | } from "./SpecificationExtension"; 8 | 9 | export function getExtension( 10 | obj: ISpecificationExtension, 11 | extensionName: string 12 | ): any { 13 | if (SpecificationExtension.isValidExtension(extensionName)) { 14 | return obj[extensionName]; 15 | } 16 | return undefined; 17 | } 18 | export function addExtension( 19 | obj: ISpecificationExtension, 20 | extensionName: string, 21 | extension: any 22 | ): void { 23 | if (SpecificationExtension.isValidExtension(extensionName)) { 24 | obj[extensionName] = extension; 25 | } 26 | } 27 | 28 | export interface OpenAPIObject extends ISpecificationExtension { 29 | openapi: string; 30 | info: InfoObject; 31 | servers?: ServerObject[]; 32 | paths: PathsObject; 33 | components?: ComponentsObject; 34 | security?: SecurityRequirementObject[]; 35 | tags?: TagObject[]; 36 | externalDocs?: ExternalDocumentationObject; 37 | } 38 | export interface InfoObject extends ISpecificationExtension { 39 | title: string; 40 | description?: string; 41 | termsOfService?: string; 42 | contact?: ContactObject; 43 | license?: LicenseObject; 44 | version: string; 45 | } 46 | export interface ContactObject extends ISpecificationExtension { 47 | name?: string; 48 | url?: string; 49 | email?: string; 50 | } 51 | export interface LicenseObject extends ISpecificationExtension { 52 | name: string; 53 | url?: string; 54 | } 55 | export interface ServerObject extends ISpecificationExtension { 56 | url: string; 57 | description?: string; 58 | variables?: { [v: string]: ServerVariableObject }; 59 | } 60 | export interface ServerVariableObject extends ISpecificationExtension { 61 | enum?: string[] | boolean[] | number[]; 62 | default: string | boolean | number; 63 | description?: string; 64 | } 65 | export interface ComponentsObject extends ISpecificationExtension { 66 | schemas?: { [schema: string]: SchemaObject | ReferenceObject }; 67 | responses?: { [response: string]: ResponseObject | ReferenceObject }; 68 | parameters?: { [parameter: string]: ParameterObject | ReferenceObject }; 69 | examples?: { [example: string]: ExampleObject | ReferenceObject }; 70 | requestBodies?: { [request: string]: RequestBodyObject | ReferenceObject }; 71 | headers?: { [header: string]: HeaderObject | ReferenceObject }; 72 | securitySchemes?: { 73 | [securityScheme: string]: SecuritySchemeObject | ReferenceObject; 74 | }; 75 | links?: { [link: string]: LinkObject | ReferenceObject }; 76 | callbacks?: { [callback: string]: CallbackObject | ReferenceObject }; 77 | } 78 | 79 | /** 80 | * Rename it to Paths Object to be consistent with the spec 81 | * See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#pathsObject 82 | */ 83 | export interface PathsObject extends ISpecificationExtension { 84 | // [path: string]: PathItemObject; 85 | [path: string]: PathItemObject | any; // Hack for allowing ISpecificationExtension 86 | } 87 | 88 | /** 89 | * @deprecated 90 | * Create a type alias for backward compatibility 91 | */ 92 | export type PathObject = PathsObject; 93 | 94 | export function getPath( 95 | pathsObject: PathsObject, 96 | path: string 97 | ): PathItemObject | undefined { 98 | if (SpecificationExtension.isValidExtension(path)) { 99 | return undefined; 100 | } 101 | return pathsObject[path] as PathItemObject; 102 | } 103 | 104 | export interface PathItemObject extends ISpecificationExtension { 105 | $ref?: string; 106 | summary?: string; 107 | description?: string; 108 | get?: OperationObject; 109 | put?: OperationObject; 110 | post?: OperationObject; 111 | delete?: OperationObject; 112 | options?: OperationObject; 113 | head?: OperationObject; 114 | patch?: OperationObject; 115 | trace?: OperationObject; 116 | servers?: ServerObject[]; 117 | parameters?: (ParameterObject | ReferenceObject)[]; 118 | } 119 | export interface OperationObject extends ISpecificationExtension { 120 | tags?: (string | undefined)[]; 121 | summary?: string; 122 | description?: string; 123 | externalDocs?: ExternalDocumentationObject; 124 | operationId?: string; 125 | parameters?: (ParameterObject | ReferenceObject)[]; 126 | requestBody?: RequestBodyObject | ReferenceObject; 127 | responses: ResponsesObject; 128 | callbacks?: CallbacksObject; 129 | deprecated?: boolean; 130 | security?: SecurityRequirementObject[]; 131 | servers?: ServerObject[]; 132 | } 133 | export interface ExternalDocumentationObject extends ISpecificationExtension { 134 | description?: string; 135 | url: string; 136 | } 137 | 138 | /** 139 | * The location of a parameter. 140 | * Possible values are "query", "header", "path" or "cookie". 141 | * Specification: 142 | * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-locations 143 | */ 144 | export type ParameterLocation = "query" | "header" | "path" | "cookie"; 145 | 146 | /** 147 | * The style of a parameter. 148 | * Describes how the parameter value will be serialized. 149 | * (serialization is not implemented yet) 150 | * Specification: 151 | * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#style-values 152 | */ 153 | export type ParameterStyle = 154 | | "matrix" 155 | | "label" 156 | | "form" 157 | | "simple" 158 | | "spaceDelimited" 159 | | "pipeDelimited" 160 | | "deepObject"; 161 | 162 | export interface BaseParameterObject extends ISpecificationExtension { 163 | description?: string; 164 | required?: boolean; 165 | deprecated?: boolean; 166 | allowEmptyValue?: boolean; 167 | 168 | style?: ParameterStyle; // "matrix" | "label" | "form" | "simple" | "spaceDelimited" | "pipeDelimited" | "deepObject"; 169 | explode?: boolean; 170 | allowReserved?: boolean; 171 | schema?: SchemaObject | ReferenceObject; 172 | examples?: { [param: string]: ExampleObject | ReferenceObject }; 173 | example?: any; 174 | content?: ContentObject; 175 | } 176 | 177 | export interface ParameterObject extends BaseParameterObject { 178 | name: string; 179 | in: ParameterLocation; // "query" | "header" | "path" | "cookie"; 180 | } 181 | export interface RequestBodyObject extends ISpecificationExtension { 182 | description?: string; 183 | content: ContentObject; 184 | required?: boolean; 185 | } 186 | export interface ContentObject { 187 | [mediatype: string]: MediaTypeObject; 188 | } 189 | export interface MediaTypeObject extends ISpecificationExtension { 190 | schema?: SchemaObject | ReferenceObject; 191 | examples?: ExamplesObject; 192 | example?: any; 193 | encoding?: EncodingObject; 194 | } 195 | export interface EncodingObject extends ISpecificationExtension { 196 | // [property: string]: EncodingPropertyObject; 197 | [property: string]: EncodingPropertyObject | any; // Hack for allowing ISpecificationExtension 198 | } 199 | export interface EncodingPropertyObject { 200 | contentType?: string; 201 | headers?: { [key: string]: HeaderObject | ReferenceObject }; 202 | style?: string; 203 | explode?: boolean; 204 | allowReserved?: boolean; 205 | [key: string]: any; // (any) = Hack for allowing ISpecificationExtension 206 | } 207 | export interface ResponsesObject extends ISpecificationExtension { 208 | default?: ResponseObject | ReferenceObject; 209 | 210 | // [statuscode: string]: ResponseObject | ReferenceObject; 211 | [statuscode: string]: ResponseObject | ReferenceObject | any; // (any) = Hack for allowing ISpecificationExtension 212 | } 213 | export interface ResponseObject extends ISpecificationExtension { 214 | description: string; 215 | headers?: HeadersObject; 216 | content?: ContentObject; 217 | links?: LinksObject; 218 | } 219 | export interface CallbacksObject extends ISpecificationExtension { 220 | // [name: string]: CallbackObject | ReferenceObject; 221 | [name: string]: CallbackObject | ReferenceObject | any; // Hack for allowing ISpecificationExtension 222 | } 223 | export interface CallbackObject extends ISpecificationExtension { 224 | // [name: string]: PathItemObject; 225 | [name: string]: PathItemObject | any; // Hack for allowing ISpecificationExtension 226 | } 227 | export interface HeadersObject { 228 | [name: string]: HeaderObject | ReferenceObject; 229 | } 230 | export interface ExampleObject { 231 | summary?: string; 232 | description?: string; 233 | value?: any; 234 | externalValue?: string; 235 | [property: string]: any; // Hack for allowing ISpecificationExtension 236 | } 237 | export interface LinksObject { 238 | [name: string]: LinkObject | ReferenceObject; 239 | } 240 | export interface LinkObject extends ISpecificationExtension { 241 | operationRef?: string; 242 | operationId?: string; 243 | parameters?: LinkParametersObject; 244 | requestBody?: any | string; 245 | description?: string; 246 | server?: ServerObject; 247 | [property: string]: any; // Hack for allowing ISpecificationExtension 248 | } 249 | export interface LinkParametersObject { 250 | [name: string]: any | string; 251 | } 252 | export interface HeaderObject extends BaseParameterObject {} 253 | export interface TagObject extends ISpecificationExtension { 254 | name: string; 255 | description?: string; 256 | externalDocs?: ExternalDocumentationObject; 257 | [extension: string]: any; // Hack for allowing ISpecificationExtension 258 | } 259 | export interface ExamplesObject { 260 | [name: string]: ExampleObject | ReferenceObject; 261 | } 262 | 263 | export interface ReferenceObject { 264 | $ref: string; 265 | } 266 | 267 | /** 268 | * A type guard to check if the given value is a `ReferenceObject`. 269 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 270 | * 271 | * @param obj The value to check. 272 | */ 273 | export function isReferenceObject(obj: object): obj is ReferenceObject { 274 | /* eslint-disable-next-line no-prototype-builtins */ 275 | return obj.hasOwnProperty("$ref"); 276 | } 277 | 278 | export interface SchemaObject extends ISpecificationExtension { 279 | nullable?: boolean; 280 | discriminator?: DiscriminatorObject; 281 | readOnly?: boolean; 282 | writeOnly?: boolean; 283 | xml?: XmlObject; 284 | externalDocs?: ExternalDocumentationObject; 285 | example?: any; 286 | examples?: any[]; 287 | deprecated?: boolean; 288 | 289 | type?: string; 290 | allOf?: (SchemaObject | ReferenceObject)[]; 291 | oneOf?: (SchemaObject | ReferenceObject)[]; 292 | anyOf?: (SchemaObject | ReferenceObject)[]; 293 | not?: SchemaObject | ReferenceObject; 294 | items?: SchemaObject | ReferenceObject; 295 | properties?: { [propertyName: string]: SchemaObject | ReferenceObject }; 296 | additionalProperties?: SchemaObject | ReferenceObject | boolean; 297 | description?: string; 298 | format?: string; 299 | default?: any; 300 | 301 | title?: string; 302 | multipleOf?: number; 303 | maximum?: number; 304 | exclusiveMaximum?: boolean; 305 | minimum?: number; 306 | exclusiveMinimum?: boolean; 307 | maxLength?: number; 308 | minLength?: number; 309 | pattern?: string; 310 | maxItems?: number; 311 | minItems?: number; 312 | uniqueItems?: boolean; 313 | maxProperties?: number; 314 | minProperties?: number; 315 | required?: string[]; 316 | enum?: any[]; 317 | } 318 | 319 | /** 320 | * A type guard to check if the given object is a `SchemaObject`. 321 | * Useful to distinguish from `ReferenceObject` values that can be used 322 | * in most places where `SchemaObject` is allowed. 323 | * 324 | * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types 325 | * 326 | * @param schema The value to check. 327 | */ 328 | export function isSchemaObject( 329 | schema: SchemaObject | ReferenceObject 330 | ): schema is SchemaObject { 331 | /* eslint-disable-next-line no-prototype-builtins */ 332 | return !schema.hasOwnProperty("$ref"); 333 | } 334 | 335 | export interface SchemasObject { 336 | [schema: string]: SchemaObject; 337 | } 338 | 339 | export interface DiscriminatorObject { 340 | propertyName: string; 341 | mapping?: { [key: string]: string }; 342 | } 343 | 344 | export interface XmlObject extends ISpecificationExtension { 345 | name?: string; 346 | namespace?: string; 347 | prefix?: string; 348 | attribute?: boolean; 349 | wrapped?: boolean; 350 | } 351 | export type SecuritySchemeType = "apiKey" | "http" | "oauth2" | "openIdConnect"; 352 | 353 | export interface SecuritySchemeObject extends ISpecificationExtension { 354 | type: SecuritySchemeType; 355 | description?: string; 356 | name?: string; // required only for apiKey 357 | in?: string; // required only for apiKey 358 | scheme?: string; // required only for http 359 | bearerFormat?: string; 360 | flows?: OAuthFlowsObject; // required only for oauth2 361 | openIdConnectUrl?: string; // required only for openIdConnect 362 | } 363 | export interface OAuthFlowsObject extends ISpecificationExtension { 364 | implicit?: OAuthFlowObject; 365 | password?: OAuthFlowObject; 366 | clientCredentials?: OAuthFlowObject; 367 | authorizationCode?: OAuthFlowObject; 368 | } 369 | export interface OAuthFlowObject extends ISpecificationExtension { 370 | authorizationUrl?: string; 371 | tokenUrl?: string; 372 | refreshUrl?: string; 373 | scopes: ScopesObject; 374 | } 375 | export interface ScopesObject extends ISpecificationExtension { 376 | [scope: string]: any; // Hack for allowing ISpecificationExtension 377 | } 378 | export interface SecurityRequirementObject { 379 | [name: string]: string[]; 380 | } 381 | -------------------------------------------------------------------------------- /src/utils/openapi3-ts/SpecificationExtension.ts: -------------------------------------------------------------------------------- 1 | // Suport for Specification Extensions 2 | // as described in 3 | // https://github.com/OAI/OpenAPI-Specification/blob/3.0.0-rc0/versions/3.0.md#specificationExtensions 4 | 5 | // Specification Extensions 6 | // ^x- 7 | export interface ISpecificationExtension { 8 | // Cannot constraint to "^x-" but can filter them later to access to them 9 | [extensionName: string]: any; 10 | } 11 | 12 | export class SpecificationExtension implements ISpecificationExtension { 13 | // Cannot constraint to "^x-" but can filter them later to access to them 14 | [extensionName: string]: any; 15 | 16 | static isValidExtension(extensionName: string) { 17 | return /^x\-/.test(extensionName); 18 | } 19 | 20 | getExtension(extensionName: string): any { 21 | if (!SpecificationExtension.isValidExtension(extensionName)) { 22 | throw new Error( 23 | "Invalid specification extension: '" + 24 | extensionName + 25 | "'. Extensions must start with prefix 'x-" 26 | ); 27 | } 28 | if (this[extensionName]) { 29 | return this[extensionName]; 30 | } 31 | return null; 32 | } 33 | addExtension(extensionName: string, payload: any): void { 34 | if (!SpecificationExtension.isValidExtension(extensionName)) { 35 | throw new Error( 36 | "Invalid specification extension: '" + 37 | extensionName + 38 | "'. Extensions must start with prefix 'x-" 39 | ); 40 | } 41 | this[extensionName] = payload; 42 | } 43 | listExtensions(): string[] { 44 | const res: string[] = []; 45 | for (const propName in this) { 46 | /* eslint-disable-next-line no-prototype-builtins */ 47 | if (this.hasOwnProperty(propName)) { 48 | if (SpecificationExtension.isValidExtension(propName)) { 49 | res.push(propName); 50 | } 51 | } 52 | } 53 | return res; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6", 6 | "es7", 7 | "esnext", 8 | "dom" 9 | ], 10 | "target": "es5", 11 | "removeComments": false, 12 | "esModuleInterop": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "strictPropertyInitialization": false, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "downlevelIteration": true, 23 | "isolatedModules": true 24 | }, 25 | "include": [ 26 | "./src/**/*", 27 | "./.eslintrc.js" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | }, 10 | "exclude": [ 11 | "./src/**/__tests__", 12 | "./src/playground.ts" 13 | ] 14 | } -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | // "outDir": "./lib/esm", 6 | "declaration": false, 7 | "declarationMap": false, 8 | "sourceMap": true, 9 | }, 10 | "exclude": [ 11 | "./src/**/__tests__", 12 | "./src/playground.ts" 13 | ] 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/types", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": [ 10 | "./src/**/__tests__", 11 | "./src/playground.ts" 12 | ] 13 | } --------------------------------------------------------------------------------