├── .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 |
2 |
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 | 
145 |
146 |
--------------------------------------------------------------------------------
/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------