[zod-openapi] Merge subapps' definitions into main app (#153)
* feat(zod-openapi): support `init` object * feat(zod-openapi): support `v3.1` spec output * feat(zod-openapi): Merge subapps' definitionspull/157/head
parent
ec3beefd63
commit
430088e175
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': minor
|
||||
---
|
||||
|
||||
Merge subapps' spec definitions into main app
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': minor
|
||||
---
|
||||
|
||||
Support v3.1 spec output
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': minor
|
||||
---
|
||||
|
||||
OpenAPIHono constructor supports init object
|
|
@ -174,6 +174,15 @@ app.openapi(
|
|||
)
|
||||
```
|
||||
|
||||
### OpenAPI v3.1
|
||||
|
||||
You can generate OpenAPI v3.1 spec using the following methods:
|
||||
|
||||
```ts
|
||||
app.doc31('/docs', {openapi: '3.1.0'}) // new endpoint
|
||||
app.getOpenAPI31Document(, {openapi: '3.1.0'}) // raw json
|
||||
```
|
||||
|
||||
### The Registry
|
||||
|
||||
You can access the [`OpenAPIRegistry`](https://github.com/asteasolutions/zod-to-openapi#the-registry) object via `app.openAPIRegistry`:
|
||||
|
@ -214,44 +223,9 @@ const client = hc<typeof appRoutes>('http://localhost:8787/')
|
|||
|
||||
## Limitations
|
||||
|
||||
An instance of Zod OpenAPI Hono cannot be used as a "subApp" in conjunction with `rootApp.route('/api', subApp)`.
|
||||
Use `app.mount('/api', subApp.fetch)` instead.
|
||||
Be careful when combining `OpenAPIHono` instances with plain `Hono` instances. `OpenAPIHono` will merge the definitions of direct subapps, but plain `Hono` knows nothing about the OpenAPI spec additions. Similarly `OpenAPIHono` will not "dig" for instances deep inside a branch of plain `Hono` instances.
|
||||
|
||||
```ts
|
||||
const api = OpenAPIHono()
|
||||
|
||||
// ...
|
||||
|
||||
// Set the `/api` as a base path in the document.
|
||||
api.get('/doc', (c) => {
|
||||
const url = new URL(c.req.url)
|
||||
url.pathname = '/api'
|
||||
url.search = ''
|
||||
|
||||
return c.json(
|
||||
// `api.getOpenAPIDocument()` will return a JSON object of the docs.
|
||||
api.getOpenAPIDocument({
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: 'My API',
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: `${url.toString()}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// Mount the Open API app to `/api` in the main app.
|
||||
app.mount('/api', api.fetch)
|
||||
|
||||
export default app
|
||||
```
|
||||
If you're migrating from plain `Hono` to `OpenAPIHono`, we recommend porting your top-level app, then working your way down the router tree.
|
||||
|
||||
## References
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
|||
ZodContentObject,
|
||||
ZodRequestBody,
|
||||
} from '@asteasolutions/zod-to-openapi'
|
||||
import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'
|
||||
import { OpenApiGeneratorV3, OpenApiGeneratorV31, 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'
|
||||
|
@ -21,6 +21,8 @@ import type {
|
|||
ToSchema,
|
||||
TypedResponse,
|
||||
} from 'hono'
|
||||
import type { MergePath, MergeSchemaPath } from 'hono/dist/types/types'
|
||||
import type { RemoveBlankRecord } from 'hono/utils/types'
|
||||
import type { AnyZodObject, ZodSchema, ZodError } from 'zod'
|
||||
import { z, ZodType } from 'zod'
|
||||
|
||||
|
@ -145,6 +147,8 @@ type ConvertPathType<T extends string> = T extends `${infer _}/{${infer Param}}$
|
|||
|
||||
type HandlerResponse<O> = TypedResponse<O> | Promise<TypedResponse<O>>
|
||||
|
||||
type HonoInit = ConstructorParameters<typeof Hono>[0];
|
||||
|
||||
export class OpenAPIHono<
|
||||
E extends Env = Env,
|
||||
S extends Schema = {},
|
||||
|
@ -152,8 +156,8 @@ export class OpenAPIHono<
|
|||
> extends Hono<E, S, BasePath> {
|
||||
openAPIRegistry: OpenAPIRegistry
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
constructor(init?: HonoInit) {
|
||||
super(init)
|
||||
this.openAPIRegistry = new OpenAPIRegistry()
|
||||
}
|
||||
|
||||
|
@ -170,7 +174,7 @@ export class OpenAPIHono<
|
|||
route: R,
|
||||
handler: Handler<E, P, I, HandlerResponse<OutputType<R>>>,
|
||||
hook?: Hook<I, E, P, OutputType<R>>
|
||||
): Hono<E, ToSchema<R['method'], P, I['in'], OutputType<R>>, BasePath> => {
|
||||
): OpenAPIHono<E, ToSchema<R['method'], P, I['in'], OutputType<R>>, BasePath> => {
|
||||
this.openAPIRegistry.registerPath(route)
|
||||
|
||||
const validators: MiddlewareHandler[] = []
|
||||
|
@ -229,12 +233,94 @@ export class OpenAPIHono<
|
|||
return document
|
||||
}
|
||||
|
||||
getOpenAPI31Document = (config: OpenAPIObjectConfig) => {
|
||||
const generator = new OpenApiGeneratorV31(this.openAPIRegistry.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)
|
||||
})
|
||||
}
|
||||
|
||||
doc31 = (path: string, config: OpenAPIObjectConfig) => {
|
||||
this.get(path, (c) => {
|
||||
const document = this.getOpenAPI31Document(config)
|
||||
return c.json(document)
|
||||
})
|
||||
}
|
||||
|
||||
route<
|
||||
SubPath extends string,
|
||||
SubEnv extends Env,
|
||||
SubSchema extends Schema,
|
||||
SubBasePath extends string
|
||||
>(
|
||||
path: SubPath,
|
||||
app: Hono<SubEnv, SubSchema, SubBasePath>
|
||||
): OpenAPIHono<E, MergeSchemaPath<SubSchema, MergePath<BasePath, SubPath>> & S, BasePath>
|
||||
route<SubPath extends string>(path: SubPath): Hono<E, RemoveBlankRecord<S>, BasePath>
|
||||
route<
|
||||
SubPath extends string,
|
||||
SubEnv extends Env,
|
||||
SubSchema extends Schema,
|
||||
SubBasePath extends string
|
||||
>(
|
||||
path: SubPath,
|
||||
app?: Hono<SubEnv, SubSchema, SubBasePath>
|
||||
): OpenAPIHono<E, MergeSchemaPath<SubSchema, MergePath<BasePath, SubPath>> & S, BasePath> {
|
||||
super.route(path, app as any)
|
||||
|
||||
if (!(app instanceof OpenAPIHono)) {
|
||||
return this as any
|
||||
}
|
||||
|
||||
app.openAPIRegistry.definitions.forEach((def) => {
|
||||
switch (def.type) {
|
||||
case 'component':
|
||||
return this.openAPIRegistry.registerComponent(
|
||||
def.componentType,
|
||||
def.name,
|
||||
def.component
|
||||
)
|
||||
|
||||
case 'route':
|
||||
return this.openAPIRegistry.registerPath({
|
||||
...def.route,
|
||||
path: `${path}${def.route.path}`
|
||||
})
|
||||
|
||||
case 'webhook':
|
||||
return this.openAPIRegistry.registerWebhook({
|
||||
...def.webhook,
|
||||
path: `${path}${def.webhook.path}`
|
||||
})
|
||||
|
||||
case 'schema':
|
||||
return this.openAPIRegistry.register(
|
||||
def.schema._def.openapi._internal.refId,
|
||||
def.schema
|
||||
)
|
||||
|
||||
case 'parameter':
|
||||
return this.openAPIRegistry.registerParameter(
|
||||
def.schema._def.openapi._internal.refId,
|
||||
def.schema
|
||||
)
|
||||
|
||||
default: {
|
||||
const errorIfNotExhaustive: never = def
|
||||
throw new Error(`Unknown registry type: ${errorIfNotExhaustive}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return this as any
|
||||
}
|
||||
}
|
||||
|
||||
export const createRoute = <P extends string, R extends Omit<RouteConfig, 'path'> & { path: P }>(
|
||||
|
|
|
@ -4,6 +4,19 @@ import type { Hono, Env, ToSchema } from 'hono'
|
|||
import { describe, it, expect, expectTypeOf } from 'vitest'
|
||||
import { OpenAPIHono, createRoute, z } from '../src'
|
||||
|
||||
describe('Constructor', () => {
|
||||
it('Should not require init object', () => {
|
||||
expect(() => new OpenAPIHono()).not.toThrow()
|
||||
})
|
||||
|
||||
it('Should accept init object', () => {
|
||||
const getPath = () => ''
|
||||
const app = new OpenAPIHono({getPath})
|
||||
|
||||
expect(app.getPath).toBe(getPath)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic - params', () => {
|
||||
const ParamsSchema = z.object({
|
||||
id: z
|
||||
|
@ -577,6 +590,78 @@ describe('Types', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Routers', () => {
|
||||
const RequestSchema = z.object({
|
||||
id: z.number().openapi({}),
|
||||
})
|
||||
|
||||
const PostSchema = z
|
||||
.object({
|
||||
id: z.number().openapi({}),
|
||||
})
|
||||
.openapi('Post')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/posts',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: RequestSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PostSchema,
|
||||
},
|
||||
},
|
||||
description: 'Post a post',
|
||||
},
|
||||
},
|
||||
})
|
||||
it('Should include definitions from nested routers', () => {
|
||||
const router = new OpenAPIHono().openapi(route, (ctx) => {
|
||||
return ctx.jsonT({id: 123})
|
||||
})
|
||||
|
||||
router.openAPIRegistry.register('Id', z.number())
|
||||
|
||||
router.openAPIRegistry.registerParameter('Key', z.number().openapi({
|
||||
param: {in: 'path'}
|
||||
}))
|
||||
|
||||
router.openAPIRegistry.registerWebhook({
|
||||
method: 'post',
|
||||
path: '/postback',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Receives a post back'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono().route('/api', router)
|
||||
const json = app.getOpenAPI31Document({
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
title: 'My API',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(json.components?.schemas).toHaveProperty('Id')
|
||||
expect(json.components?.schemas).toHaveProperty('Post')
|
||||
expect(json.components?.parameters).toHaveProperty('Key')
|
||||
expect(json.paths).toHaveProperty('/api/posts')
|
||||
expect(json.webhooks).toHaveProperty('/api/postback')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multi params', () => {
|
||||
const ParamsSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
Loading…
Reference in New Issue