parent
c18cedb1b2
commit
7b898034a5
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': patch
|
||||
---
|
||||
|
||||
introduce Zod OpenAPI
|
|
@ -0,0 +1,28 @@
|
|||
name: ci-zod-openapi
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/zod-openapi/**'
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- 'packages/zod-openapi/**'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/zod-openapi
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Build zod-validator in root directory
|
||||
run: yarn build:zod-validator
|
||||
working-directory: .
|
||||
- run: yarn build
|
||||
- run: yarn test
|
12
package.json
12
package.json
|
@ -2,9 +2,11 @@
|
|||
"name": "hono-middleware",
|
||||
"version": "0.0.0",
|
||||
"description": "Third-party middleware for Hono",
|
||||
"workspaces": [
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build:hello": "yarn workspace @hono/hello build",
|
||||
"build:zod-validator": "yarn workspace @hono/zod-validator build",
|
||||
|
@ -16,6 +18,7 @@
|
|||
"build:typebox-validator": "yarn workspace @hono/typebox-validator build",
|
||||
"build:medley-router": "yarn workspace @hono/medley-router build",
|
||||
"build:valibot-validator": "yarn workspace @hono/valibot-validator build",
|
||||
"build:zod-openapi": "yarn workspace @hono/zod-openapi build",
|
||||
"build": "run-p build:*"
|
||||
},
|
||||
"license": "MIT",
|
||||
|
@ -43,8 +46,11 @@
|
|||
"jest-environment-miniflare": "^2.10.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"publint": "^0.2.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typescript": "^4.7.4"
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vitest": "^0.34.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
# Zod OpenAPI Hono
|
||||
|
||||
A wrapper class for Hono that supports OpenAPI. With it, you can validate values and types using [Zod](https://zod.dev/) and generate OpenAPI Swagger documentation.
|
||||
This is based on [Zod to OpenAPI](https://github.com/asteasolutions/zod-to-openapi).
|
||||
For details on creating schemas and defining routes, please refer to this resource.
|
||||
|
||||
_This is not a middleware but hosted on this monorepo_
|
||||
|
||||
## Usage
|
||||
|
||||
### Install
|
||||
|
||||
```
|
||||
npm i hono zod @hono/zod-openapi
|
||||
```
|
||||
|
||||
### Write your application
|
||||
|
||||
Define schemas:
|
||||
|
||||
```ts
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.min(3)
|
||||
.openapi({
|
||||
param: {
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
},
|
||||
example: '1212121',
|
||||
}),
|
||||
})
|
||||
|
||||
const UserSchema = z
|
||||
.object({
|
||||
id: z.number().openapi({
|
||||
example: 123,
|
||||
}),
|
||||
name: z.string().openapi({
|
||||
example: 'John Doe',
|
||||
}),
|
||||
age: z.number().openapi({
|
||||
example: 42,
|
||||
}),
|
||||
})
|
||||
.openapi('User')
|
||||
```
|
||||
|
||||
Create routes:
|
||||
|
||||
```ts
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/users/:id',
|
||||
request: {
|
||||
params: ParamsSchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UserSchema,
|
||||
},
|
||||
},
|
||||
description: 'Get the user',
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
description: 'Error!',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Create the App:
|
||||
|
||||
```ts
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(
|
||||
route,
|
||||
(c) => {
|
||||
const { id } = c.req.valid('param')
|
||||
return c.jsonT({
|
||||
id: Number(id),
|
||||
age: 20,
|
||||
name: 'Ultra-man',
|
||||
})
|
||||
},
|
||||
(result, c) => {
|
||||
if (!result.success) {
|
||||
const res = c.jsonT(
|
||||
{
|
||||
ok: false,
|
||||
},
|
||||
400
|
||||
)
|
||||
return res
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
app.doc('/doc', {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: 'My API',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Yusuke Wada <https://github.com/yusukebe>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "@hono/zod-openapi",
|
||||
"version": "0.0.0",
|
||||
"description": "A wrapper class of Hono which supports OpenAPI.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
||||
"publint": "publint",
|
||||
"release": "yarn build && yarn test && yarn publint && yarn publish"
|
||||
},
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org",
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/honojs/middleware.git"
|
||||
},
|
||||
"homepage": "https://github.com/honojs/middleware",
|
||||
"peerDependencies": {
|
||||
"hono": "*",
|
||||
"zod": "3.*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"hono": "^3.4.3",
|
||||
"zod": "^3.22.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "^5.5.0",
|
||||
"@hono/zod-validator": "^0.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type {
|
||||
ResponseConfig,
|
||||
RouteConfig,
|
||||
ZodContentObject,
|
||||
ZodRequestBody,
|
||||
} from '@asteasolutions/zod-to-openapi'
|
||||
import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'
|
||||
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'
|
||||
import type { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import type { Context, Input, TypedResponse } from 'hono'
|
||||
import type { Env, Handler, MiddlewareHandler } from 'hono'
|
||||
import type { AnyZodObject, ZodSchema, ZodError } from 'zod'
|
||||
import { z, ZodType } from 'zod'
|
||||
|
||||
type RequestTypes = {
|
||||
body?: ZodRequestBody
|
||||
params?: AnyZodObject
|
||||
query?: AnyZodObject
|
||||
cookies?: AnyZodObject // not support
|
||||
headers?: AnyZodObject | ZodType<unknown>[] // not support
|
||||
}
|
||||
|
||||
type IsJson<T> = T extends string
|
||||
? T extends `application/json${infer _Rest}`
|
||||
? 'json'
|
||||
: never
|
||||
: never
|
||||
|
||||
type IsForm<T> = T extends string
|
||||
? T extends
|
||||
| `multipart/form-data${infer _Rest}`
|
||||
| `application/x-www-form-urlencoded${infer _Rest}`
|
||||
? 'form'
|
||||
: never
|
||||
: never
|
||||
|
||||
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request']
|
||||
? R['request'][Part]
|
||||
: never
|
||||
|
||||
type InputTypeBase<
|
||||
R extends RouteConfig,
|
||||
Part extends string,
|
||||
Type extends string
|
||||
> = R['request'] extends RequestTypes
|
||||
? RequestPart<R, Part> extends AnyZodObject
|
||||
? {
|
||||
out: { [K in Type]: z.input<RequestPart<R, Part>> }
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
|
||||
type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes
|
||||
? R['request']['body'] extends ZodRequestBody
|
||||
? R['request']['body']['content'] extends ZodContentObject
|
||||
? IsJson<keyof R['request']['body']['content']> extends never
|
||||
? {}
|
||||
: R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any>
|
||||
? {
|
||||
out: {
|
||||
json: z.input<
|
||||
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
|
||||
>
|
||||
}
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
|
||||
type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes
|
||||
? R['request']['body'] extends ZodRequestBody
|
||||
? R['request']['body']['content'] extends ZodContentObject
|
||||
? IsForm<keyof R['request']['body']['content']> extends never
|
||||
? {}
|
||||
: R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any>
|
||||
? {
|
||||
out: {
|
||||
form: z.input<
|
||||
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
|
||||
>
|
||||
}
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
|
||||
type InputTypeParam<R extends RouteConfig> = InputTypeBase<R, 'params', 'param'>
|
||||
type InputTypeQuery<R extends RouteConfig> = InputTypeBase<R, 'query', 'query'>
|
||||
|
||||
type OutputType<R extends RouteConfig> = R['responses'] extends Record<infer _, infer C>
|
||||
? C extends ResponseConfig
|
||||
? C['content'] extends ZodContentObject
|
||||
? IsJson<keyof C['content']> extends never
|
||||
? {}
|
||||
: C['content'][keyof C['content']]['schema'] extends ZodSchema
|
||||
? z.infer<C['content'][keyof C['content']]['schema']>
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
|
||||
type Hook<T, E extends Env, P extends string, O> = (
|
||||
result:
|
||||
| {
|
||||
success: true
|
||||
data: T
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: ZodError
|
||||
},
|
||||
c: Context<E, P>
|
||||
) => TypedResponse<O> | Promise<TypedResponse<T>> | void
|
||||
|
||||
export class OpenAPIHono<E extends Env = Env, S = {}, BasePath extends string = '/'> extends Hono<
|
||||
E,
|
||||
S,
|
||||
BasePath
|
||||
> {
|
||||
#registry: OpenAPIRegistry
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.#registry = new OpenAPIRegistry()
|
||||
}
|
||||
|
||||
openapi = <
|
||||
R extends RouteConfig,
|
||||
I extends Input = InputTypeParam<R> & InputTypeQuery<R> & InputTypeForm<R> & InputTypeJson<R>
|
||||
>(
|
||||
route: R,
|
||||
handler: Handler<E, R['path'], I, OutputType<R>>,
|
||||
hook?: Hook<I, E, R['path'], OutputType<R>>
|
||||
) => {
|
||||
this.#registry.registerPath(route)
|
||||
|
||||
const validators: MiddlewareHandler[] = []
|
||||
|
||||
if (route.request?.query) {
|
||||
const validator = zValidator('query', route.request.query as any, hook as any)
|
||||
validators.push(validator as any)
|
||||
}
|
||||
|
||||
if (route.request?.params) {
|
||||
const validator = zValidator('param', route.request.params as any, hook as any)
|
||||
validators.push(validator as any)
|
||||
}
|
||||
|
||||
const bodyContent = route.request?.body?.content
|
||||
|
||||
if (bodyContent) {
|
||||
for (const mediaType of Object.keys(bodyContent)) {
|
||||
if (mediaType.startsWith('application/json')) {
|
||||
const schema = bodyContent[mediaType]['schema']
|
||||
if (schema instanceof ZodType) {
|
||||
const validator = zValidator('json', schema as any, hook as any)
|
||||
validators.push(validator as any)
|
||||
}
|
||||
}
|
||||
if (
|
||||
mediaType.startsWith('multipart/form-data') ||
|
||||
mediaType.startsWith('application/x-www-form-urlencoded')
|
||||
) {
|
||||
const schema = bodyContent[mediaType]['schema']
|
||||
if (schema instanceof ZodType) {
|
||||
const validator = zValidator('form', schema as any, hook as any)
|
||||
validators.push(validator as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.on([route.method], route.path, ...validators, handler)
|
||||
return this
|
||||
}
|
||||
|
||||
getOpenAPIDocument = (config: OpenAPIObjectConfig) => {
|
||||
const generator = new OpenApiGeneratorV3(this.#registry.definitions)
|
||||
const document = generator.generateDocument(config)
|
||||
return document
|
||||
}
|
||||
|
||||
doc = (path: string, config: OpenAPIObjectConfig) => {
|
||||
this.get(path, (c) => {
|
||||
const document = this.getOpenAPIDocument(config)
|
||||
return c.json(document)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const createRoute = <P extends string, R extends Omit<RouteConfig, 'path'> & { path: P }>(
|
||||
routeConfig: R
|
||||
) => routeConfig
|
||||
|
||||
extendZodWithOpenApi(z)
|
||||
export { z }
|
|
@ -0,0 +1,384 @@
|
|||
// eslint-disable-next-line node/no-extraneous-import
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { OpenAPIHono, createRoute, z } from '../src'
|
||||
|
||||
describe('Basic - params', () => {
|
||||
const ParamsSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.min(3)
|
||||
.openapi({
|
||||
param: {
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
},
|
||||
example: '1212121',
|
||||
}),
|
||||
})
|
||||
|
||||
const UserSchema = z
|
||||
.object({
|
||||
id: z.number().openapi({
|
||||
example: 123,
|
||||
}),
|
||||
name: z.string().openapi({
|
||||
example: 'John Doe',
|
||||
}),
|
||||
age: z.number().openapi({
|
||||
example: 42,
|
||||
}),
|
||||
})
|
||||
.openapi('User')
|
||||
|
||||
const ErrorSchema = z
|
||||
.object({
|
||||
ok: z.boolean().openapi({
|
||||
example: false,
|
||||
}),
|
||||
})
|
||||
.openapi('Error')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/users/:id',
|
||||
request: {
|
||||
params: ParamsSchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UserSchema,
|
||||
},
|
||||
},
|
||||
description: 'Get the user',
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
description: 'Error!',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(
|
||||
route,
|
||||
(c) => {
|
||||
const { id } = c.req.valid('param')
|
||||
return c.jsonT({
|
||||
id: Number(id),
|
||||
age: 20,
|
||||
name: 'Ultra-man',
|
||||
})
|
||||
},
|
||||
(result, c) => {
|
||||
if (!result.success) {
|
||||
const res = c.jsonT(
|
||||
{
|
||||
ok: false,
|
||||
},
|
||||
400
|
||||
)
|
||||
return res
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
app.doc('/doc', {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: 'My API',
|
||||
},
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct contents', async () => {
|
||||
const res = await app.request('/users/123')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
id: 123,
|
||||
age: 20,
|
||||
name: 'Ultra-man',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
const res = await app.request('/users/1')
|
||||
expect(res.status).toBe(400)
|
||||
expect(await res.json()).toEqual({ ok: false })
|
||||
})
|
||||
|
||||
it('Should return OpenAPI documents', async () => {
|
||||
const res = await app.request('/doc')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
openapi: '3.0.0',
|
||||
info: { version: '1.0.0', title: 'My API' },
|
||||
components: {
|
||||
schemas: {
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', example: 123 },
|
||||
name: { type: 'string', example: 'John Doe' },
|
||||
age: { type: 'number', example: 42 },
|
||||
},
|
||||
required: ['id', 'name', 'age'],
|
||||
},
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: { ok: { type: 'boolean', example: false } },
|
||||
required: ['ok'],
|
||||
},
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
paths: {
|
||||
'/users/:id': {
|
||||
get: {
|
||||
parameters: [
|
||||
{
|
||||
schema: { type: 'string', minLength: 3, example: '1212121' },
|
||||
required: true,
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Get the user',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/User' } } },
|
||||
},
|
||||
'400': {
|
||||
description: 'Error!',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query', () => {
|
||||
const QuerySchema = z.object({
|
||||
page: z.string().openapi({
|
||||
example: '123',
|
||||
}),
|
||||
})
|
||||
|
||||
const PostsSchema = z
|
||||
.object({
|
||||
title: z.string().openapi({}),
|
||||
content: z.string().openapi({}),
|
||||
page: z.number().openapi({}),
|
||||
})
|
||||
.openapi('Post')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/books',
|
||||
request: {
|
||||
query: QuerySchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PostsSchema,
|
||||
},
|
||||
},
|
||||
description: 'Get the posts',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const { page } = c.req.valid('query')
|
||||
return c.jsonT({
|
||||
title: 'Good title',
|
||||
content: 'Good content',
|
||||
page: Number(page),
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct contents', async () => {
|
||||
const res = await app.request('/books?page=123')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
title: 'Good title',
|
||||
content: 'Good content',
|
||||
page: 123,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
const res = await app.request('/books')
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', () => {
|
||||
const RequestSchema = z.object({
|
||||
id: z.number().openapi({}),
|
||||
title: z.string().openapi({}),
|
||||
})
|
||||
|
||||
const PostsSchema = z
|
||||
.object({
|
||||
id: z.number().openapi({}),
|
||||
title: z.string().openapi({}),
|
||||
})
|
||||
.openapi('Post')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/posts',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: RequestSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PostsSchema,
|
||||
},
|
||||
},
|
||||
description: 'Get the posts',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const { id, title } = c.req.valid('json')
|
||||
return c.jsonT({
|
||||
id,
|
||||
title,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct contents', async () => {
|
||||
const req = new Request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: 123,
|
||||
title: 'Good title',
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await app.request(req)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
id: 123,
|
||||
title: 'Good title',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
const req = new Request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form', () => {
|
||||
const RequestSchema = z.object({
|
||||
id: z.string().openapi({}),
|
||||
title: z.string().openapi({}),
|
||||
})
|
||||
|
||||
const PostsSchema = z
|
||||
.object({
|
||||
id: z.number().openapi({}),
|
||||
title: z.string().openapi({}),
|
||||
})
|
||||
.openapi('Post')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/posts',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/x-www-form-urlencoded': {
|
||||
schema: RequestSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PostsSchema,
|
||||
},
|
||||
},
|
||||
description: 'Get the posts',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const { id, title } = c.req.valid('form')
|
||||
return c.jsonT({
|
||||
id: Number(id),
|
||||
title,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct contents', async () => {
|
||||
const searchParams = new URLSearchParams()
|
||||
searchParams.append('id', '123')
|
||||
searchParams.append('title', 'Good title')
|
||||
const req = new Request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
body: searchParams,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await app.request(req)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
id: 123,
|
||||
title: 'Good title',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
const req = new Request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
}
|
Loading…
Reference in New Issue