Skip to content

Commit

Permalink
Support OpenAPI (#153)
Browse files Browse the repository at this point in the history
* ダミーのSwagger UIとopenapiの配信エンドポイントを追加

* Refine vite-react-openapi

* WIP: Add valibot openapi integration

* Implement toOpenApiEndpoint

* Implement toOpenApiEndpoints

* OpenAPI 3.1

* valibot spec to jsonschema spec

* Use json schema7

* Refine valibot structure and more

* Add JsonSchemaOpenApiEndpoints

* ResponseにOpenAPIのメタ情報を追加できるようにした

* PathItemとOperationObjectのマッピングが間違っていたのを修正

* Support requestBody

* paramsやqueryを元にparametersを生成

* Fix express openapi example

* Support zod-openapi

* Fix zod-openapi test

* ApiEndpointsからからjson schemaへの変換処理を共通化

* Add zod-openapi

* Update OpenAPI document
  • Loading branch information
mpppk authored Jan 24, 2025
1 parent c12b9c8 commit b24322a
Show file tree
Hide file tree
Showing 34 changed files with 3,835 additions and 217 deletions.
70 changes: 70 additions & 0 deletions examples/misc/express/valibot/openapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import express from "express";
import * as v from "valibot";
import cors from "cors";
import { OpenAPIV3_1 } from "openapi-types";
import { ValibotOpenApiEndpoints } from "@notainc/typed-api-spec/valibot/openapi";
import { toOpenApiDoc } from "@notainc/typed-api-spec/valibot";

const openapiBaseDoc: Omit<OpenAPIV3_1.Document, "paths"> = {
openapi: "3.1.0",
servers: [{ url: "http://locahost:3000" }],
info: {
title: "typed-api-spec OpenAPI Example",
version: "1",
description:
"This is a sample Pet Store Server based on the OpenAPI 3.1 specification.",
},
tags: [{ name: "pets", description: "Everything about your Pets" }],
};

const apiEndpoints = {
"/pets/:petId": {
get: {
summary: "Find pet by ID",
description: "Returns a single pet",
tags: ["pets"],
params: v.object({ petId: v.string() }),
query: v.object({ page: v.string() }),
responses: {
200: {
body: v.object({ name: v.string() }),
description: "List of pets",
},
},
},
},
"/pets": {
post: {
description: "Add new pet",
body: v.object({ name: v.string() }),
responses: {
200: {
body: v.object({ message: v.string() }),
description: "Created pet",
},
},
},
},
} satisfies ValibotOpenApiEndpoints;

const newApp = () => {
const app = express();
app.use(express.json());
app.use(cors());
// const wApp = asAsync(typed(apiEndpoints, app));
app.get("/openapi", (req, res) => {
const openapi = toOpenApiDoc(openapiBaseDoc, apiEndpoints);
res.status(200).json(openapi);
});
return app;
};

const main = async () => {
const app = newApp();
const port = 3000;
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
};

main();
73 changes: 73 additions & 0 deletions examples/misc/express/zod/openapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import express from "express";
import cors from "cors";
import { OpenAPIV3_1 } from "openapi-types";
import "zod-openapi/extend";
import z from "zod";
import { toOpenApiDoc } from "@notainc/typed-api-spec/zod/openapi";
import { ZodOpenApiEndpoints } from "@notainc/typed-api-spec/zod/openapi";

const openapiBaseDoc: Omit<OpenAPIV3_1.Document, "paths"> = {
openapi: "3.1.0",
servers: [{ url: "http://locahost:3000" }],
info: {
title: "typed-api-spec OpenAPI Example",
version: "1",
description:
"This is a sample Pet Store Server based on the OpenAPI 3.1 specification.",
},
tags: [{ name: "pets", description: "Everything about your Pets" }],
};

const apiEndpoints = {
"/pets/:petId": {
get: {
summary: "Find pet by ID",
description: "Returns a single pet",
tags: ["pets"],
params: z.object({
petId: z.string().openapi({ description: "ID of pet", example: "1" }),
}),
query: z.object({ page: z.string() }),
responses: {
200: {
body: z.object({ name: z.string() }),
description: "List of pets",
},
},
},
},
"/pets": {
post: {
description: "Add new pet",
body: z.object({ name: z.string() }),
responses: {
200: {
body: z.object({ message: z.string() }),
description: "Created pet",
},
},
},
},
} satisfies ZodOpenApiEndpoints;

const newApp = () => {
const app = express();
app.use(express.json());
app.use(cors());
// const wApp = asAsync(typed(apiEndpoints, app));
app.get("/openapi", (req, res) => {
const openapi = toOpenApiDoc(openapiBaseDoc, apiEndpoints);
res.status(200).json(openapi);
});
return app;
};

const main = async () => {
const app = newApp();
const port = 3000;
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
};

main();
6 changes: 5 additions & 1 deletion examples/misc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,24 @@
"test:type-check": "tsc --noEmit",
"ex:express:zod:server": "tsx express/zod/express.ts",
"ex:express:zod:client": "tsx express/zod/fetch.ts",
"ex:express:zod:openapi": "tsx express/zod/openapi/index.ts",
"ex:express:valibot:server": "tsx express/valibot/express.ts",
"ex:express:valibot:client": "tsx express/valibot/fetch.ts",
"ex:express:valibot:openapi": "tsx express/valibot/openapi/index.ts",
"ex:fastify:zod:server": "tsx fastify/zod/fastify.ts",
"ex:fasitify:zod:client": "tsx fastify/zod/fetch.ts",
"ex:withValidation": "tsx simple/withValidation.ts"
},
"dependencies": {
"@types/express": "^4",
"cors": "^2.8.5",
"express": "^4",
"fastify-type-provider-zod": "^2.1.0",
"valibot": "^0",
"valibot": "1.0.0-beta.11",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.57.0",
Expand Down
24 changes: 24 additions & 0 deletions examples/vite-react-openapi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
50 changes: 50 additions & 0 deletions examples/vite-react-openapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
28 changes: 28 additions & 0 deletions examples/vite-react-openapi/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
13 changes: 13 additions & 0 deletions examples/vite-react-openapi/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
31 changes: 31 additions & 0 deletions examples/vite-react-openapi/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "vite-react-openapi",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"swagger-ui-react": "^5.18.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swagger-ui-react": "^4.18.3",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
}
}
10 changes: 10 additions & 0 deletions examples/vite-react-openapi/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import SwaggerUI from "swagger-ui-react"
import "swagger-ui-react/swagger-ui.css"

createRoot(document.getElementById('root')!).render(
<StrictMode>
<SwaggerUI url="http://localhost:3000/openapi" />
</StrictMode>,
)
1 change: 1 addition & 0 deletions examples/vite-react-openapi/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
26 changes: 26 additions & 0 deletions examples/vite-react-openapi/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
7 changes: 7 additions & 0 deletions examples/vite-react-openapi/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
24 changes: 24 additions & 0 deletions examples/vite-react-openapi/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
Loading

0 comments on commit b24322a

Please sign in to comment.